JAVA线程安全和锁的原理

Synchronized和Lock

Posted by     MrCodeSniper on September 4, 2017

本文首次发布于 My Blog, 作者 @陈鸿(My) ,转载请保留原文链接.

什么是线程安全

多线程访问同个资源时,会导致资源出现异常 需要对这种情况进行管理

通常多线程访问时,采用加锁机制能妥善管理资源

ps:线程中有个变量为ThreadLocal 是线程的私有数据 主要用于线程改变内部的数据时不影响其他线程(自身的副本),使用时需要注意static。

ps:clone()基本类型直接拷值 对象来说在堆内存创建一模一样的新对象(地址不同)

1.深拷贝 拷贝的对象里面引用的对象再拷贝, 2.浅拷贝 只拷贝当前对象 对于对象里的引用保持

java提供的线程安全机制

修饰符volatile

volatile被设计用来修饰被不同线程访问和修改的变量

1.可见性:

volatile可以保证在一个线程的工作内存中修改了该变量的值,该变量的值立即能回显到主内存中,从而保证所有的线程看到这个变量的值是一致的

但其不具备原子性 不适合在对变量的写操作依赖于变量本身自己 但是可以在操作完将值赋给其他变量 例如num=i+1; 若i是valatile修饰那么num是

线程安全的变量

2.有序性

禁止进行指令重排序

它确保指令重排序时不会把其后面的指令排到其之前的位置,也不会把前面的指令排到其后

即在执行到用到volatile这句指令时,在它前面的操作已经全部完成。

synchronized关键字 属于互斥锁

用法: 1.修饰普通方法 2.修饰静态方法 3.修饰代码块

这里我们用代码与执行结果来讨论

1.修饰普通方法

	final Sync sync1=new Sync();
        final Sync sync2=new Sync();
    public synchronized void method1(){//
        System.out.println("method1 start");
        try{
            System.out.println("method1 excute");
            Thread.sleep(3000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("method1 end");
    }

    public  synchronized void method2(){
        System.out.println("method2 start");
        try{
            System.out.println("method2 excute");
            Thread.sleep(1000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("method2 end");
    }

开启两个子线程分别执行对象1的方法1与方法2

//    method1 start
//    method1 excute
//    method1 end
//    method2 start
//    method2 excute
//    method2 end

结果同步成功

这是因为synchronized修饰方法 会获得this的锁 也就是调用method1,2的对象sync1 拿到的是对象锁 所以同步成功

2.修饰静态方法


public static synchronized void method1()

public static synchronized void method2()

这里我们来调用sync1的method1 和sync2的method2,结果为

//    method1 start
//    method1 excute
//    method1 end
//    method2 start
//    method2 excute
//    method2 end

与之前的结果相同也就是同步成功,这是怎么回事呢?不同的对象线程同步

因为对静态方法的同步本质上是对类的同步 虽然他们属于不同的对象 但属于同个类 这里获取的是类锁 所以只能顺序执行

3.修饰代码块

 public void method1(){
        System.out.println("method1 start");
        try{
            synchronized(this){
                System.out.println("method1 excute");
                Thread.sleep(3000);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("method1 end");
    }

    public void method2(){
        System.out.println("method2 start");
        try{
            synchronized(this){
                System.out.println("method2 excute");
                Thread.sleep(1000);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("method2 end");
    }

我们调用async1的方法1和方法2 我们看看结果

method1 start
method1 excute
method2 start
method1 end
method2 excute
method2 end

看上去是没实现同步的样子 其实是同步的

每个对象都有一个锁 当锁被占用就会处于锁定状态

这里因为先执行method1所以method1 start 到同步代码块执行method1 excute 这时占用了对象的锁

继续执行线程睡眠 这阶段的同时method2也在执行 会执行method2 start 但经过代码块时获取不到锁 线程阻塞

当锁的owner线程执行完时调用method1 end 后 method2的线程又获取了对象的锁 继续执行剩下的操作

Lock

java为我们提供了Lock框架,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为Lock的多种实现留下了空间

ReentrantLock 可重入锁

所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的

ReentrantLock 类实现了Lock ,它拥有与synchronized 相同的并发性和内存语义 但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性

Lock需要我们手动释放锁 所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内!!

特性

1.公平性

ReentrantLock构造函数中提供公平性锁和非公平锁(默认)两种选择

所谓公平锁,线程将按照他们发出请求的顺序来获取锁,不允许插队

但在非公平锁上,则允许插队:当一个线程发生获取锁的请求的时刻,如果这个锁是可用的,那这个线程将跳过所在队列里等待线程并获得锁

非公平锁效率更高 能更及时的执行线程

2.轮询锁的和定时锁

死锁的很多情况是由于顺序锁引起的, 不同线程在试图获得锁的时候阻塞,并且不释放自己已经持有的锁, 最后造成死锁

tryLock()方法在试图获得锁的时候,如果该锁已经被其它线程持有,则按照设置方式立刻返回,而不是一直阻塞等下去,同时在返回后释放自己持有的锁.可以根据返回的结果进行重试或者取消,进而避免死锁的发生.

ReadWriteLock 读写锁

假设这样的场景 我们操作数据 数据集的读取和写入是应该互斥 但读取和读取之间需要互斥吗?

Thread-R2准备读取数据  
Thread-R2读取1  
Thread-R0准备读取数据 //R0和R2可以同时读取,不应该互斥!  
Thread-R0读取1  

R0和R2在读数据时是同步的 但在我们的观点来看读互斥没有必要,这种时候需要读写锁来实现

 rwl.writeLock().lock();// 取到写锁    
 rwl.writeLock().unlock();// 释放写锁    
 rwl.readLock().lock();// 取到读锁   
 rwl.readLock().unlock();// 释放读锁    

使用读写锁替换互斥锁和重入锁,结果

Thread-R2准备读取数据  
Thread-R1准备读取数据  
Thread-R0准备读取数据  
Thread-R0读取11  
Thread-R1读取11  
Thread-R2读取11  

三个线程都是同时读取数据的,与互斥锁定相比,读-写锁定允许对共享数据进行更高级别的并发访问

适合在对不经常修改的集合频繁检索的场景

死锁 -必须避免

class Thread01 extends Thread{
        private Object resource01;
        private Object resource02;
        public Thread01(Object resource01, Object resource02) {
            this.resource01 = resource01;
            this.resource02 = resource02;
        }
        @Override
        public void run() {
            synchronized(resource01){
                System.out.println("Thread01 locked resource01");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (resource02) {
                    System.out.println("Thread01 locked resource02");
                }
            }
        }
    }
    class Thread02 extends Thread{
        private Object resource01;
        private Object resource02;
        public Thread02(Object resource01, Object resource02) {
            this.resource01 = resource01;
            this.resource02 = resource02;

        }
        @Override
        public void run() {
            synchronized(resource02){
                System.out.println("Thread02 locked resource02");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (resource01) {
                    System.out.println("Thread02 locked resource01");
                }
            }
        }
    }
    
public static void main(String[] args) {
        final Object resource01 = "resource01";
        final Object resource02 = "resource02";
        Thread01 thread01 = new Thread01(resource01, resource02);
        Thread02 thread02 = new Thread02(resource01, resource02);
        thread01.start();
        thread02.start();
    }   

我们可以看到线程1,2共享资源1,2

执行主函数 线程1执行时会获得资源1的锁 并进入sleep 这时候线程2会获得资源2的锁 并进入sleep 线程1结束sleep 想请求 resource2的锁 但被thread2占用而进入堵塞,thread2想获得resource1的锁但被thread1占用而堵塞 双方都在等待资源 导致线程死锁 焦灼下去

Thread01 locked resource01
Thread02 locked resource02

原理解析

JVM管理线程一些相关的成员

Owner:获得锁的线程称为Owner !Owner:释放锁的线程 onDeck:就绪线程

等待队列 -1.竞争队列 -2.候选队列

就绪线程

运行线程

阻塞队列

机制:

线程请求被锁住的class 对象 代码块等 需要先请求锁 而新请求锁的线程将首先被加入到竞争队列中, 竞争队列会被线程并发访问 当持有锁的线程释放这个锁 会从竞争队列中迁移线程到 候选队列 并会指定候选中的某个线程为就绪线程

当运行的线程调用wait被阻塞 会进入阻塞线程队列 若阻塞队列的线程被notify,notifyall则转移到 候选队列

synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。

sleep和wait的区别

sleep是线程的方法 wait是object类的方法

sleep源码

public static void sleep(long millis, int nanos)
    throws InterruptedException {
      
        long start = System.nanoTime();
        long duration = (millis * NANOS_PER_MILLI) + nanos;

        Object lock = currentThread().lock;

        // Wait may return early, so loop until sleep duration passes.
        synchronized (lock) {
            while (true) {
                sleep(lock, millis, nanos);

                long now = System.nanoTime();
                long elapsed = now - start;

		//一直循环 保持线程锁的占有 直到时间结束
                if (elapsed >= duration) {
                    break;
                }

                duration -= elapsed;
                start = now;
                millis = duration / NANOS_PER_MILLI;
                nanos = (int) (duration % NANOS_PER_MILLI);
            }
        }
    }

当一个线程执行到wait方法时 可以让同步方法或者同步块暂时放弃对象锁,而将它暂时让给其它需要对象锁的人 它就进入到一个和该对象相关的等待池,同时释放对象的锁,使得其他线程能够访问 可以通过notify,notifyAll方法来唤醒等待的线程

Thread执行sleep时 会获取线程的锁并 使用synchronized 保持对锁的占有 并循环保持 直到时间到 再释放

生产者-消费者模型

缓冲区代码 维护了一个容量为10的链表 多线程放产品取产品都要经过同步

public class BufferLocation {

    private int maxSize;
    private List<Date> storage;
    private Date date;

    public BufferLocation() {
        maxSize=10;
        storage=new LinkedList<>();
    }
 
    //数据要一个个取
    public synchronized void get(){//得到BufferLocation对象锁
        while (storage.size()==0){
            try {
                wait();//可以让同步方法或者同步块暂时放弃对象锁,而将它暂时让给其它需要对象锁的人
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
	System.out.printf("Get: %d: %s",storage.size(),((LinkedList<?>)storage).poll());
        notifyAll();//通知wait的线程
    }

    
    //数据要一个个放
    public synchronized void  put(Date date){
        while (storage.size()==maxSize){//满了要阻塞
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.date=date;
        if(this.date==null) {
            date=new Date();
        }
        storage.add(date);
        notifyAll();//通知正在等待的其他线程
    } 
}

生产者

public class Producer implements Runnable{

    private BufferLocation bufferLocation;

    public Producer(BufferLocation bufferLocation) {
        this.bufferLocation = bufferLocation;
    }

    @Override
    public void run() {
        for(int i = 0; i < 100; i++) {
            bufferLocation.put(new Date());
        }
    }
}

消费者

public class Consumer implements Runnable {

    private BufferLocation bufferLocation;


    public Consumer(BufferLocation bufferLocation) {
        this.bufferLocation = bufferLocation;
    }

    @Override
    public void run() {
        for(int i = 0; i < 100; i++) {
            bufferLocation.get();
        }
    }
}

给生产者和消费者两个线程同时运行

首先生产者进入put方法 因为被synchronized 占有this的锁也就是调用者bufferLocation的锁

若是缓冲区满 则循环执行wait 线程进入等待队列,释放对bufferLocation锁  由消费者线程消费

若是缓冲区未满 则加入数据 调用notifyAll() 唤醒此线程 重新获得bufferLocation锁 继续执行

再是消费者进入get方法 因为被synchronized 占有this的锁也就是调用者bufferLocation的锁

若是缓冲区为空 则循环执行wait 线程进入等待队列,释放对bufferLocation锁  由生产者线程生产

若缓冲区不为空 则取出数据 唤醒此线程 重新获得bufferLocation锁 继续执行

end

关于 线程安全 的更多研究,可以 看这里