1-引入synchronized锁

2020-02-04  本文已影响0人  鹏程1995

引入

线程安全问题

在java的多线程的编程中,经常出现线程安全的问题,主要就在于正确性:即多线程对临界资源进行访问时,如果不进行控制,每个线程拿到的值都不能确定是有效的,依赖这个值进行的判断或者计算也就可能会造成错误。

临界资源

指会被多个并发线程/进程进行竞争性访问的共享的、可变的资源【个人的理解】

实现线程安全的思路及手段

引发线程安全的一个例子

1.png

线程X在访问后线程Y对临界资源进行了修改。随后线程X用计算值覆盖A时,由于读取的A已经是错误的了,计算结果也就是错的。由此引发了错误。

解决的思路

解决问题的关键就是在线程的角度来说,线程拿到临界资源访问权限后必须保证在自己完成自己的相关操作前,临界资源的值是有效的。也就是说,此线程的操作不允许进行打断和其他操作的插入。需要采用同步机制来协同对临界资源的访问。即在同一时刻,只能有一个线程访问临界资源,也称作 同步互斥访问。从线程的角度来说,是将线程的相关操处理成原子的操作

实现这个效果有两个方法synchronized,lock。本文章是lock源码的引入文章,主要介绍synchronized以及synchronizedlock实现原理的区别。

synchronized

用法

synchronized互斥锁,即 能到达到互斥访问目的的锁。举个简单的例子,如果对临界资源加上互斥锁,当一个线程在访问该临界资源时,其他线程便只能等待。

synchronized可以加在方法或者代码块上,这样在线程执行此段代码时必须要获得该锁。

代码示例:

class A{
    public synchronized void hehe(){
        xxxxxxxxx
    }
    
    public void haha(){
        synchronized(xx){
            xxxxxxxxx
        }
    }
}

大概就是这么用的。下面将进行介绍

注意点

synchronized针对的是线程

介绍

synchronized针对的是访问对象的线程,即当一个线程获得这个对象的互斥锁时,其他的线程无法访问此对象。

示例

class B {
    
    public synchronzed void methodA(){
        return null;        
    }
    
    public synchronized void methodB(){
        this.methodA();
    }
    
    public static void main(String[] args){
        new A().methodB();
    }
}

此示例是可以正确执行的。执行的有以下特点:

  1. 当主线程在执行methodB()时,其他的线程无法执行methodB(),methodA()。即只能同时有一个线程获得此对象的互斥锁

  2. 主线程获得互斥锁,在执行this.methodA();时是可以执行的,因为此线程已经获得了锁,从代码上看,获得锁的线程可以再次获得同一个锁,即synchronized锁是可重入的

  3. 在方法上用synchronized,默认用的是此对象的锁。如果是用代码块的话,就得指出用的哪个对象的锁。

synchronized的获得和释放

锁的获得

进入synchronized标注的方法或者代码块即认为获得锁

锁的释放

情况一、正常执行

执行完代码块或者synchronized即认为释放了锁,要么是顺序执行执行完了,要么是抛了异常直接从synchronized代码块出去了。

情况二、wait(),notify(),notifyAll()
wait()
  • 前置条件:线程获得了锁

  • 行为:当前线程挂起等待并释放锁

  • 后置条件:此线程保持挂起状态,等待设置的等待时间到达或者被notify(),notify唤醒后重新进入到此锁的竞争中

notify()
  • 前置条件:线程获得了锁
  • 行为:唤醒一个因为wait()方法而挂起的线程并使其参与到锁的竞争中本线程不必释放锁
  • 后置条件:本线程正常占用锁并执行。wait()挂起的线程被唤醒一个
notifyAll()

notify()一致,只是这个是唤醒所有的wait()方法挂起的线程。

注意

notify(),notifyAll()只是唤醒,真的能不能获得锁要看线程的调度或者看竞争能不能竞争的上。

synchronized锁定的是对象

对方法使用

如上的class B代码。直接在实例方法前加synchronized关键词,表示要执行此方法的线程必须先获得此对象的锁。

对代码块使用

如上的class A代码。自己写一个synchronized块即可。

代码示例一

借用大佬博客的一段代码:


class Sync {
 
    public synchronized void test() {
        System.out.println("test开始..");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("test结束..");
    }
}
 
class MyThread extends Thread {
 
    public void run() {
        Sync sync = new Sync();
        sync.test();
    }
}
 
public class Main {
 
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            Thread thread = new MyThread();
            thread.start();
        }
    }
}

执行结果是:

test开始..
test开始..
test开始..
test结束..
test结束..
test结束..

不是我们期待的,原因是synchronized锁定的是对象,三个线程的run()中各自new了三个Sync对象,各用各的锁,所以没有互相阻塞。

将代码修改:


public void test() {
    synchronized(this){
        System.out.println("test开始..");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("test结束..");
    }
}

情况不便,还是用自己的锁,三个对象三个锁。

继续修改:


class MyThread extends Thread {
 
    private Sync sync;
 
    public MyThread(Sync sync) {
        this.sync = sync;
    }
 
    public void run() {
        sync.test();
    }
}
 
public class Main {
 
    public static void main(String[] args) {
        Sync sync = new Sync();
        for (int i = 0; i < 3; i++) {
            Thread thread = new MyThread(sync);
            thread.start();
        }
    }
}

一个对象一个锁,三个线程竞争,所以输出如下:

test开始..
test结束..
test开始..
test结束..
test开始..
test结束..

继续修改,静态的锁:


class Sync {
 
    public void test() {
        synchronized (Sync.class) {
            System.out.println("test开始..");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("test结束..");
        }
    }
}

锁住的是静态的Sync.classsynchronized(xx.class)static synchronized一样。一个xx.class就一个锁,所以他们也相当于全局锁

注意:锁的使用重点是是不是同一个锁以方便同步,具体是谁的锁,叫啥名无所谓,上面那个例子你用synchronized(Object.class)也行,只是可能会顺手阻塞一大把无关线程

比较常见的例子是锁一个字符串,如:

synchronized("哈哈哈"){
    
}

会将字符串常量池锁住,一锁锁一片,会严重影响程序的执行效率。

代码示例二

class A{
    
}

class C implements Runnable{
    private A fieldA;
    
    C(A input){
        this.fieldA = input;
    }
    
    public void run(){
        synchronized(fieldA){
            //哈哈哈
        }
    }
    
    public static void main(String[] args){
        A a = new A();
        Thread thread1 = new Thread(new C(a));
        Thread thread2 = new Thread(new C(a));
        Thread thread3 = new Thread(new C(new A()));
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

注意:synchronized锁住的是对象而不是对象的引用。其中thread1,thread2用的都是对象a的锁,会互斥,即使都是new出来的不同的C实例的field。最后一个是新的A不是同一个锁,不会互斥。

synchronized不锁定对象的所有东西

介绍

synchronized只是一个用来标出代码块的锁,即使是同一个对象,如果有的方法你没标synchronized,也是可以多线程同时访问的。

实例

class C{
    public synchronized void method1(){
        
    }
    
    public void method2(){
        
    }
}

两个Thread分别访问method1(),method2()不会引起阻塞。

静态的synchronized

介绍

synchronized锁住的是对象实例或者类。多个实例,他们的锁之间不会影响,因为每个实例都有自己的锁。类锁之间会影响,因为同一个类只有一个类锁。

实例

上面“针对线程”的那一节有介绍。

底层实现方法

synchronized是java的关键字,在JVM层面实现了对临界资源的同步互斥访问,在编译时会在代码块的开头加 monitorenter在代码块结尾加**monitorexit **。

引用大佬博客的一段话:

monitorenter指令执行时会让对象的锁计数加1,而monitorexit指令执行时会让对象的锁计数减1,其实这个与操作系统里面的PV操作很像,操作系统里面的PV操作就是用来控制多个进程对临界资源的访问。对于synchronized方法,执行中的线程识别该方法的 method_info结构是否有 ACC_SYNCHRONIZED 标记设置,然后它自动获取对象的锁,调用方法,最后释放锁。如果有异常发生,线程自动释放锁。

有一点要注意:对于 synchronized方法 或者synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。

lock

引入lock的原因——lock的优点

粒度更细

我们上面介绍了synchronized,他在jvm层面实现了对临界资源的互斥访问,但是很多时候我们为了性能的需要想更好的定制互斥的粒度,比如对磁盘文件的读写操作,不同线程的读操作之间就没有必要做的互斥,写操作和写操作、读操作和写操作互斥即可。

可定制等待锁的时间

synchronized的块中可以通过wait()释放锁并挂起,但是很多时候等待IO时或者调用sleep()时,程序会睡眠等待,白白占用锁,导致很多线程一直等待。lock提供了可以限制锁的等待时间的操作,超时则直接不再等待;lock还增加了 新的解决方案,可以相应中断并释放锁。

可控制获得和释放顺序

synchronized的锁的获得和释放是通过进入、退出代码块来实现的。这就意味着互斥锁的获得释放遵循栈的顺序。如果我想实现下面锁的获得、释放顺序呢?

  1. 获得锁A
  2. 获得锁B
  3. 释放锁A
  4. 获得锁C
  5. 释放锁B
  6. 释放锁C

虽然这样的操作是不太好的,这种锁的顺序容易引起死锁。但是。。。。。保不准就有这样的需求是吧。

监听锁的状态

synchronized把代码块框起来之后要不线程是卡在代码块这里,要不就是在执行,无法获得锁的状态。而lock提供了可以查看是否能获得锁的操作。

注意事项——lock的缺点

锁不会主动释放

synchronized锁只要你退出代码块就会退出锁,不管是执行完还是异常抛出去,都能够及时的将锁释放。lock锁在遇到异常时不会结束锁,必须手动释放。如果没有做释放就容易一直占着锁。【lock锁不限制时间、不设置可打断的情况下】

用法

lock()

阻塞方法,尝试获得锁,获得不到就等着获得锁。

如果采用lock(),必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此,一般来说,使用lock必须在try…catch…块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放。

Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){

}finally{
    lock.unlock();   //释放锁
}

tryLock()

非阻塞方法,直接返回,获得锁返回true,锁已被占用则返回false

lock()一样,一定一定要记得释放锁

tryLock(long time, TimeUnit unit)

阻塞方法,获得锁,如果锁被占用则等待传入的时间,还获得不到则返回false。可以响应中断。如果在传入的时间内得到锁则返回true

lockInterruptibly()

阻塞方法,等待获得锁。等待锁的过程中可以相应中断。

如果B线程在等待锁,其他的线程调用了ThreadB.interrupt()会造成B线程在lockInterruptibly()处抛出InterruptedException并停止等待。

注意:当一个线程获得锁之后,是不会被interrupt()中断的

其他

引用博主的一段话:

最佳实践 (Best Practice):在使用Lock时,无论以哪种方式获取锁,习惯上最好一律将获取锁的代码放到 try…catch…,因为我们一般将锁的unlock操作放到finally子句中,如果线程没有获取到锁,在执行finally子句时,就会执行unlock操作,从而抛出 IllegalMonitorStateException,因为该线程并未获得到锁却执行了解锁操作。

参考文献

synchronized:

lock:

上一篇 下一篇

猜你喜欢

热点阅读