java线程锁策略

1.线程不安全问题及原因

1.1什么叫做线程不安全问题?

数据的不一致:在多线程的执行中,程序的执行结果与预期不相符
代码示例如下:

public class ThreadDemo15 {     static class Counter{         private int num = 0;         private final int maxSize = 100000;         public void increment(){             for (int i = 0; i <maxSize ; i++) {                 num++;             }         }         public void decrement(){             for (int i = 0; i <maxSize ; i++) {                 num--;             }         }         public int getNum(){             return num;         }     }       public static void main(String[] args) throws InterruptedException {         Counter counter = new Counter();         Thread t1 = new Thread(()->{             counter.increment();         });         t1.start();         Thread t2 = new Thread(()->{            counter.decrement();         });         t2.start();         t1.join();         t2.join();           System.out.println("结果"+counter.getNum());     } } 

这里我们期望的结果是0;但是每次执行都会出现不同的结果
java线程锁策略

1.2 导致线程非安全的原因

1.CPU强占执行,
2.非原子性:原子性–指一个操作是不可中断的,即使多个线程一起执行的时候,一个操作一旦开始就不会被其他线程干扰;而非原子性就是指线程操作被其他线程干扰;
3.编译器优化:在单线程下可以提升程序的执行效率,但是在多线程下就会出现混乱,从而导致线程不安全的问题;–指令重排序
4.内存可见性问题:线程A对共享变量进行了操作,但没有及时把更改后的值存入主内存中,而此时线程B从主内存读取到了共享变量的值,所以X的值是原始值,此时对于线程B来说,共享变量的改变对于B是不可见的,
5.多个线程同时修改同一个变量

理解非原子性问题:

public class ThreadDemo15 {     static class Counter{         private int num = 0;         private final int maxSize = 100000;         public void increment(){             for (int i = 0; i <maxSize ; i++) {                 num++;             }         }         public void decrement(){             for (int i = 0; i <maxSize ; i++) {                 num--;             }         }         public int getNum(){             return num;         }     }      public static void main(String[] args) throws InterruptedException {         Counter counter = new Counter();         Thread t1 = new Thread(()->{             counter.increment();         });         t1.start();         Thread t2 = new Thread(()->{            counter.decrement();         });         t2.start();         t1.join();         t2.join();         System.out.println("结果"+counter.getNum());     } } 

java线程锁策略
按照我们设计的线程执行结果应该是num=0;但这里每一次执行的num都不相同,
这就是由于线程之间的操作的原子性导致的;
线程处理数据的过程:
1.从主内存中加载数据到工作内存,
2.对工作内存中的内容进行操作
3.操作完成之后就对这些数据写入主内存;
两个线程都改变了num的值,因而在操作过程中,出现了两次操作混乱的情况;
而导致上述问题就是由于两个线程操作内存的顺序混乱;

2.线程不安全问题的解决方案

1.volatile 作用:
A.禁止指令重排序,
B.解决线程可见性问题–每次线程操作完变量之后,强制删除掉工作内存中的变量
注意:不能解决原子性问题;因此不能解决上述问题
2.加锁:
A.public class ThreadDemo17 { private static int num = 0; private static final int maxSize = 100000; public static void main(String[] args) throws InterruptedException { Object lock = new Object(); Object lock2 = new Object(); Thread t1 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i <maxSize ; i++) { synchronized (lock){ num++; } } } }); t1.start(); Thread t2 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i <maxSize ; i++) { synchronized (lock){ num--; } } } }); t2.start(); t1.join(); t2.join(); System.out.println(num); } }

java线程锁策略
如果加了两把锁,跟没加锁一样:
java线程锁策略
使用锁就可以让线程在拥有锁的时候才能执行,没有锁就不能执行;

2.1.2种使用场景

synchronized关键字最主要有以下3种应用方式,下面分别介绍

  • 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁

  • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

  • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁

  1. 修饰实例方法:使用synchronized修饰了increase方法,new了两个不同的实例对象时就有两个不同的锁
public synchronized void increase(){          i++;  } 
  1. 修饰静态方法:当synchronized作用于静态方法时,其锁就是当前类的class对象锁。
public static synchronized void increase(){          i++;  } 
  1. 修饰代码块:将synchronized作用于一个给定的实例对象instance,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有instance实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待
public void run() {         //省略其他耗时操作....         //使用同步代码块对变量i进行同步操作,锁对象为instance         synchronized(instance){             for(int j=0;j<1000000;j++){                     i++;               }         }     } 

2.1.3 synchronized原理解析;

首先理解JAVA中对象头和Monitor
在JVM中,对象在内存中的布局分为三块区域,对象头、实例数据和对齐填充
java线程锁策略

  • 实例变量:存放类的属性数据java线程锁策略

    monitor:
    当线程进入被synchronized修饰的方法或代码块:指定的锁对象就会通过一些操作将对象头中的Lockword指向monitor的起始地址,同时monitor中owner存放拥有当前锁的线程标识,确保一次只能由一个线程执行该部分的代码,线程在获取锁之前不允许执行该部分代码

2.2 lock手动锁

java种的ReentrantLock实现了手动锁
通常书写格式如下:

//加锁 lock.lock(); try{     num++; }finally { //释放锁       lock.unlock(); } 

使用示例

public class ThreadDemo18 {     private static int num = 0;     private static final int maxSize = 100000;       public static void main(String[] args) throws InterruptedException {         //创建手动锁         Lock lock = new ReentrantLock();         Thread t1 = new Thread(new Runnable() {             @Override             public void run() {                 for (int i = 0; i <maxSize ; i++) {                     lock.lock();                     try{                         num++;                     }finally {                         lock.unlock();                     }                       }             }         });         t1.start();         Thread t2 = new Thread(new Runnable() {             @Override             public void run() {                 for (int i = 0; i <maxSize ; i++) {                     lock.lock();                     try{                         num--;                     }finally {                         lock.unlock();                     }                   }             }         });         t2.start();         t1.join();         t2.join();         System.out.println(num);     } } 

java线程锁策略
与synchronized效果相同

3. 面试常问

3.1 synchronized和lock的区别:

synchronized是非公平锁,

A.synchronized可以修饰代码块、静态方法、实例方法,Lock只能修饰代码块
B.sychronized只有非公平锁策略,而Lock即可以是公平锁,也可以是非公平锁;
C.sychronized是自动加锁和释放锁,lock需要手动加锁和释放锁;
公平锁可以按顺序执行,而非公平锁强占式执行,效率更高;
在java中所有的锁默认位非公平锁;
lock默认非公平锁,但可以实现公平锁
示例如下:

public class ThreadDemo19 {     public static void main(String[] args) throws InterruptedException {         Lock lock = new ReentrantLock(true);         Runnable runnable =new Runnable() {             @Override             public void run() {                 for (char item:"ABCD".toCharArray()){                     lock.lock();                     try{System.out.print(item);                     }finally {                         lock.unlock();                     }                   }             }         };         Thread t1 = new Thread(runnable,"t1");         Thread t2 = new Thread(runnable,"t2"); //        Thread.sleep(10);         t1.start();         t2.start();     } } 

公平锁可以按照顺序执行线程;
java线程锁策略

3.2 volatile和synchronized的区别?

volatile可以解决内存可见性问题和禁止指令重排序,但不能解决原子性问题,synchronized用来保证线程安全,可以解决所有线程安全的问题,–始终一个线程执行锁操作;

版权声明:玥玥 发表于 2021-08-05 18:29:08。
转载请注明:java线程锁策略 | 女黑客导航