java保证线程安全的两种方式

JVM有主内存(Main Memory)和工作内存(Working Memory),主内存就是平时所说的java堆内存,存放程序中所有的类实例、静态数据等变量,是线程共享的,而工作内存中存放的是从主内存中拷贝过来的变量以及访问方法所取得的局部变量,是每个线程独立所有的,其他线程不能访问。

每个线程都有自己的执行空间(即工作内存),线程执行的时候用到某变量,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作:读取,修改,赋值等,这些均在工作内存完成,操作完成后再将变量写回主内存;

各个线程都从主内存中获取数据,线程之间数据是不可见的;打个比方:主内存变量A原始值为1,线程1从主内存取出变量A,修改A的值为2,在线程1未将变量A写回主内存的时候,线程2拿到变量A的值仍然为1;

这便引出“可见性”的概念:当一个共享变量在多个线程的工作内存中都有副本时,如果一个线程修改了这个共享变量的副本值,那么其他线程应该能够看到这个被修改后的值,这就是多线程的可见性问题。

普通变量情况:如线程A修改了一个普通变量的值,然后向主内存进行写回,另外一条线程B在线程A回写完成了之后再从主内存进行读取操作,新变量的值才会对线程B可见;

如下图:

Java内存模型定义了8种操作来完成关于主内存和工作内存之间具体的交互,这些操作都是原子的,不可分割(long double类型除外)。这8种操作如下所示:

1) lock(锁定) 作用于主内存的变量,它把一个变量标志为一条线程独占的状态
2) unlock(解锁) 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其它线程锁定
3) read(读取) 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
4) load(载入) 作用于工作内存的变量,它把read操作从主内存得到的变量值放入工作内存的变量副本中
5) use(使用) 作用于工作内存的变量,它把变量副本的值传递给执行引擎,每当虚拟机遇到一个需要使用的变量的值的字节码指令时,将会执行这个操作。
6) assign(赋值) 作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作副本变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
7) store(存储) 作用于工作内存的变量,将工作副本变量的值传输给主内存,以便随后的write操作使用
8) write(写入) 作用于主内存的变量, 它把store操作从工作内存得到的变量的值放入主内存的变量
  如果要把一个变量从主内存复制到工作内存,那就要按顺序地执行read和load操作,如果要把变量从工作内存同步回主内存,那就要顺序地执行store和write操作。注意,Java内存模型只要求上述两个操作必须按顺序地执行,而没有保证必须是连续执行,也就是说read和load之间,store和write之间是可以插入其它指令的,如对内存中的变量a,b进行访问时,一种可能出现的顺序是read a, read b, load b, load a。

当有多条线程同时访问共享数据时,如果不进行同步,就会发生错误,java提供了多种机制保证线程同步,这里主要说下synchronized和Lock;

一.synchronized关键字:

 最简单的方式是加入synchronized关键字,只要将操作共享数据的语句加入synchronized关键字,在某一时段

只会让一个线程执行完,在执行过程中,其他线程不能进来执行:


import static java.lang.System.out;
public class Counting {
    public static void main(String[] args)
    throws InterruptedException {
        class Count{
            private int count=0;
            public <span style="color:#ff6666;">synchronized</span> void increment(){count++;}
            public int getCount(){return count;}
 
        }
        final Count counter=new Count();
        class CountingThread extends Thread {
            public void run(){
                for (int i=0; i<1000;i++ ) {
                    counter.increment();
                }
            }
        }
        CountingThread t1=new CountingThread();
        CountingThread t2=new CountingThread();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        out.println(counter.getCount());
    }
    
}
方法声明中同步(synchronized )关键字。当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。遵循以下五条原则:

一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

二、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。

三、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。

四、第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。

五、以上规则对其它对象锁同样适用。

还有,synchronized 锁机制存在重入的特性,就是可以重复获取同一个对象的锁。如下:


public synchronized void methodA(int a, int b);
 
public synchronized void methodB(int a){
         methodA(a, 0);
}
B方法可以执行,就是说B方法获得锁之后,调用的A方法也可以获得该锁。


二.使用锁:


import static java.lang.System.out;
import java.util.concurrent.locks.ReentrantLock;
 
public class thread02 {
    public static int count=0;
    public static void main(String[] args) throws InterruptedException{
 
        ReentrantLock lock=new ReentrantLock();
 
        class My_thread01 extends Thread{
            public void run(){
                lock.lock();
                try{
                    for(int i=0; i<10000;i++) {
                        count++;
                        }    
                    }
                    finally{
                        lock.unlock();
                    }
            }
        }
        class My_thread02 extends Thread{
            public void run(){
                lock.lock();
                try{
                    for(int i=0; i<10000;i++) {
                        count++;
                        }    
                    }
                    finally{
                        lock.unlock();
                    }
            }
        }
        My_thread01 t1=new My_thread01();
        My_thread02 t2=new My_thread02();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        out.println("count is "+count);
    }
    
}

区别:

a.Lock使用起来比较灵活,但需要手动释放和开启;采用synchronized不需要用户去手动释放锁,

当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;

b.Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;

c.在并发量比较小的情况下,使用synchronized是个不错的选择,但是在并发量比较高的情况下,其性能下降很严重,此时Lock是个不错的方案。

d.使用Lock的时候,等待/通知 是使用的Condition对象的await()/signal()/signalAll()  ,而使用synchronized的时候,则是对象的wait()/notify()/notifyAll();由此可以看出,使用Lock的时候,粒度更细了,一个Lock可以对应多个Condition。

e.虽然Lock缺少了synchronized隐式获取释放锁的便捷性,但是却拥有了锁获取与是释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized所不具备的同步特性;
 

发布了202 篇原创文章 · 获赞 88 · 访问量 28万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章