JAVA多线程基础 之二 什么是线程安全及解决办法

    xiaoxiao2022-06-25  220

    什么是线程安全?

    保证线程安全需要保证几个基本特性:

    原子性:相关操作不会被其他线程所打扰,一般通过同步机制实现。可见性:一个线程修改了某个共享变量,其状态能够立即被其他线程知晓。通常被解释为将线程本地状态反映到主内存上,volatile就是负责保证可见性的。有序性:保证线程内的串行语义,避免指令重排等。

     

    线程安全解决办法?

    内置的锁(synchronized)

    Java提供了一种内置的锁(Intrinsic Lock)机制来支持原子性,每一个Java对象都可以用作一个实现同步的锁,称为内置锁,也叫隐式锁。

    线程进入同步代码块之前自动获取到锁,代码块执行完成正常退出或代码块中抛出异常退出时会释放掉锁。

    内置锁为互斥锁,即线程A获取到锁后,线程B阻塞直到线程A释放锁,线程B才能获取到同一个锁。

    synchronize是可重入锁。

    内置锁使用synchronized关键字实现,synchronized关键字有两种用法:

    1.同步方法

    在方法上修饰synchronized 称为同步方法,此时充当锁的对象为调用同步方法的对象。

    非静态同步函数使用this锁。

    //非静态synchronized修饰方法 使用的是thisprivate synchronized void sale() {     try {         Thread.sleep(10);     } catch (InterruptedException e) {         e.printStackTrace();     }     if (ticketCount > 0) //最后一次抢票 会产生负数 需在此再判断一次         System.out.println(Thread.currentThread().getName() + "售出第 " + (100-ticketCount+1) + "张票");         ticketCount--;}

    静态同步函数

    方法上加上static关键字,使用synchronized关键字修饰或者使用类.class文件。

    静态的同步函数使用的锁是该函数所属字节码文件对象

    可以用getClass方法获取,也可以用当前类名.class 表示。

    //static synchronized == synchronized (DeadLockDemo.class) 锁为当前的字节码文件     private static synchronized void sale() {         try {             Thread.sleep(10);         } catch (InterruptedException e) {             e.printStackTrace();         }         if (ticketCount > 0) //最后一次抢票 会产生负数 需在此再判断一次             System.out.println(Thread.currentThread().getName() + "售出第 " + (100-ticketCount+1) + "张票");         ticketCount--;     }

    2.同步代码块

    synchronize(任意全局变量){需要被同步的代码}

    和直接使用synchronized修饰需要同步的方法是一样的,但是锁的粒度可以更细,并且充当锁的对象不一定是this,也可以是其它对象,所以使用起来更加灵活。

    synchronized (obj) {     if (ticketCount > 0) //最后一次抢票 会产生负数 需在此再判断一次         System.out.println(Thread.currentThread().getName() + "售出第 " + (100 - ticketCount + 1) + "张票");         ticketCount--;}

    上例中使用Object充当锁对象。

    synchronized(this){     try {         Thread.sleep(10);     } catch (InterruptedException e) {         e.printStackTrace();     }     if (ticketCount > 0) //最后一次抢票 会产生负数 需在此再判断一次         System.out.println(Thread.currentThread().getName() + "售出第 " + (100-ticketCount+1) + "张票");     ticketCount--;}

    上例中使用this锁。同非静态同步函数

    注意: 保证多线程同步时必须保证多个线程使用同一个锁 。

    3.底层实现

    synchronized代码块是由一对monitorenter/monitorexit指令实现的,Monitor对象是同步的基本实现单元。

    JVM提供了三种不同的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁。

    4.锁的升级降级

    所谓锁的升级降级,就是JVM优化synchronized运行的机制,当JVM检测到不同的竞争状态状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。

    没有竞争出现的时候,默认会使用偏斜锁。JVM会利用CAS操作(compare and swap),在对象头上的Mark word部分设置线程ID,以表示对象偏向于当前线程,所以不涉及真正的互斥锁。

    这样做的假设是基于在很多应用场景中,大部分对象生命周期最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。

    如果有另外的线程试图锁定某个已经被偏离过的对象,JVM就要撤销(revoke)这个对象的偏斜锁,并切换到轻量级锁实现。轻量级锁依赖CAS操作Mark Word来试图获取锁,如果重试成功,则使用普通的轻量级锁;否则,进一步升级为重量级锁。

    当JVM进入安全点(safepoint)的时候,会检查是否有闲置的Monitor,然后试图降级。

    偏斜锁、轻量级锁、重量级锁的代码实现并不在核心类库中,而是在JVM的代码中。

    关闭偏斜锁:

    -XX:-UseBiasedLocking

    5.常见问题

    不要使用String常量加锁

    注意:不要使用String常量加锁,会引起死循环的问题。

    public class StringLock {     public void method(){         //使用字符串常量加锁 只进入t1         synchronized ("lock")         //使用字符串常量加锁 如下则没有问题         synchronized (new String("lock"))         {             try {                 while (true)                 {                     System.out.println(Thread.currentThread().getId()+"------thread start--------");                     Thread.sleep(1000);                     System.out.println(Thread.currentThread().getId()+"------thread end----------");                 }             } catch (InterruptedException e) {                 e.printStackTrace();             }         }     }     public static void main(String[] args){         StringLock stringLock = new StringLock();         Thread t1 = new Thread(new Runnable() {             @Override             public void run() {                 stringLock.method();             }         });         Thread t2 = new Thread(new Runnable() {             @Override             public void run() {                 stringLock.method();             }         });         t1.start();         t2.start();     } }

    不要修改锁

    可以修改其成员变量,但不要修改其引用。修改了引用就会释放锁。

    显示锁(Lock)

    Lock是一个接口,提供了无条件的、可轮询的、定时的、可中断的锁获取操作,所有的加锁和解锁操作方法都是显示的,因而称为显示锁。

    下面我们来分析Lock的几个常见的实现类ReentrantLock、ReentrantReadWriteLock.ReadLock和ReentrantReadWriteLock.WriteLock。

    重入锁 ReentrantLock

    当一个线程试图获取一个他已经获取的锁的时候,这个获取的动作自动成功。这是一个获取锁粒度的概念,也就是锁的持有以线程为单位,并不基于调用次数。

    编码中需要注意必须要明确调用unlock()方法释放,不然就会一直持有该锁。

    重入锁可设置公平性(默认为非公平)

    ReentrantLock fairLock = new ReentrantLock(true);fairLock.lock(); try{     //do something}finally {     fairLock.unlock();}

    若使用Synchronized则无法进行公平性选择。它永远都是非公平的。

    若要保证公平性则会引入额外开销,会导致一定的吞吐量下降,因此只有程序确实有公平性需要的时候才有必要指定它。

    读写锁 ReentrantReadWriteLock

    ReadWriteLock(读写锁)是一个接口,提供了readLock和writeLock两种锁的操作,也就是说一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程。也就是说读写锁应用的场景是一个资源被大量读取操作,而只有少量的写操作。我们先看其源码:

    public interface ReadWriteLock {

        Lock readLock();

        Lock writeLock();

    }

    从源码看出,ReadWriteLock借助Lock来实现读写两个锁并存、互斥的机制。每次读取共享数据就需要读取锁,需要修改共享数据就需要写入锁。

    读写锁的机制:

    1、读-读不互斥,读线程可以并发执行;

    2、读-写互斥,有写线程时,读线程会堵塞;

    3、写-写互斥,写线程都是互斥的。

    举栗子:

    public class ReentrantReadWriteLockDemo {     public static void main(String[] args) {         final Queue3 q3 = new Queue3();         for (int i = 0; i < 3; i++) {             new Thread(new Runnable() {                 @Override                 public void run() {                     while (true) {                         q3.get();                     }                 }             }).start();         }         for (int i = 0; i < 3; i++) {             new Thread() {                 public void run() {                     while (true) {                         q3.put(new Random().nextInt(10000));                     }                 }             }.start();         }     } }class Queue3{     private Object data = null;//共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。     private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();     public void get(){         rwl.readLock().lock();//上读锁,其他线程只能读不能写         System.out.println(Thread.currentThread().getName() + " be ready to read data!");         try {             Thread.sleep((long)(Math.random()*1000));         } catch (InterruptedException e) {             e.printStackTrace();         }         System.out.println(Thread.currentThread().getName() + "have read data :" + data);         rwl.readLock().unlock(); //释放读锁,最好放在finnaly里面     }     public void put(Object data){         rwl.writeLock().lock();//上写锁,不允许其他线程读也不允许写         System.out.println(Thread.currentThread().getName() + " be ready to write data!");         try {             Thread.sleep((long)(Math.random()*1000));         } catch (InterruptedException e) {             e.printStackTrace();         }         this.data = data;         System.out.println(Thread.currentThread().getName() + " have write data: " + data);         rwl.writeLock().unlock();//释放写锁     } }

    模拟写一个缓存器

    /**  * 使用ReentrantReadWriteLock模拟一个缓存器 * Created by zhanghaipeng on 2018/10/24.  */import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class CacheDemo {     private Map<String, Object> map = new HashMap<String, Object>();//缓存器     private ReadWriteLock rwl = new ReentrantReadWriteLock();     public static void main(String[] args) {     }     public Object get(String id){         Object value = null;         rwl.readLock().lock();//首先开启读锁,从缓存中去取         try{             value = map.get(id);             if(value == null){  //如果缓存中没有释放读锁,上写锁                 rwl.readLock().unlock();                 rwl.writeLock().lock();                 try{                     if(value == null){                         value = "aaa"//此时可以去数据库中查找,这里简单的模拟一下                     }                 }finally{                     rwl.writeLock().unlock(); //释放写锁                 }                 rwl.readLock().lock(); //然后再上读锁             }         }finally{             rwl.readLock().unlock(); //最后释放读锁         }         return value;     } }

    Lock与synchronized 的比较

    Synchronized是在JVM层面上实现的,无需显示的加解锁,而ReentrantLock和ReentrantReadWriteLock需显示的加解锁,一定要保证锁资源被释放;

    Synchronized是针对一个对象的,而ReentrantLock和ReentrantReadWriteLock是代码块层面的锁定;

    ReentrantReadWriteLock和ReentrantLock的比较:

    ReentrantReadWriteLock是对ReentrantLock的复杂扩展,能适合更加复杂的业务场景,ReentrantReadWriteLock可以实现一个方法中读写分离的锁的机制。而ReentrantLock只是加锁解锁一种机制。

    ReentrantReadWriteLock引入了读写和并发机制,可以实现更复杂的锁机制,并发性相对于ReentrantLock和Synchronized更高。

    Volatile

    可见性也就是说一旦某个线程修改了该被volatile修饰的变量,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,可以立即获取修改之后的值。

    在Java中为了加快程序的运行效率,对一些变量的操作通常是在该线程的寄存器或是CPU缓存上进行的,之后才会同步到主存中,而加了volatile修饰符的变量则是直接读写主存。

    public class VolatileDemo implements Runnable {//    private Boolean flag = true;     //需添加volatile关键字     private static volatile Boolean flag = true;     @Override     public void run() {         System.out.println(Thread.currentThread().getName() + "线程开始");         while (flag)         {                 System.out.println("线程执行,flag"+flag);         }         System.out.println(Thread.currentThread().getName() + "线程结束");     }     public void setRunning(Boolean flag){         this.flag = flag ;         System.out.println("setRunning flag -->" + flag);     }     public static void main(String[] args){         VolatileDemo volatileDemo = new VolatileDemo();         Thread t1 = new Thread(volatileDemo,"t1");         t1.start();         try {             Thread.sleep(3000);         } catch (InterruptedException e) {             e.printStackTrace();         }         volatileDemo.setRunning(false);     } }

    上例中的flag需要被volatile修饰后才能保证其线程间可见。

    Volatile 保证了线程间共享变量的及时可见性,但不能保证原子性

    对异常的处理

    根据实际业务选择是否释放锁:

    方法一:记录日志并继续

    方法二:抛出异常

    public class SynchronizedException {     public synchronized void print() {         int i = 0 ;         while (true)         {             i++;             System.out.println(i);             try {                 Thread.sleep(500);             if(i==5)             {                 Integer.parseInt("a");             }             } catch (Exception e) {                 e.printStackTrace();                 //方法一:记录日志&contiunue                 System.out.println(Thread.currentThread().getId()); //                continue;                 //方法二:抛出RuntimeException()                 throw new RuntimeException();             }         }     }     public static void main(String[] args){         final SynchronizedException synchronizedException = new SynchronizedException();         Thread t = new Thread(new Runnable() {             @Override             public void run() {                 synchronizedException.print();             }         }) ;         t.start();     } }

    最新回复(0)