java线程锁策略

Posted 超分辨菜鸟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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;但是每次执行都会出现不同的结果

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());
    }
}


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

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

1.volatile 作用:
A.禁止指令重排序,
B.解决线程可见性问题–每次线程操作完变量之后,强制删除掉工作内存中的变量
注意:不能解决原子性问题;因此不能解决上述问题
2.加锁:
A.synchronized加锁和释放锁–JVM的解决方案
B.Lock手动锁–自己加锁和释放锁

2.1 synchronized的使用

synchronized:为保证操作的原子性–最好只用1把锁;

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);
    }
}


如果加了两把锁,跟没加锁一样:

使用锁就可以让线程在拥有锁的时候才能执行,没有锁就不能执行;

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中,对象在内存中的布局分为三块区域,对象头、实例数据和对齐填充

  • 实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐
  • 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐

Java对象头,是实现synchronized的锁对象的基础:
synchronized使用的锁对象是存储在JAVA对象头中的,JVM使用两个字节存储对象头,也就是mark word–分为lock
word和标志位; 标志位通常就表示当前锁的状态:

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);
    }
}


与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();
    }
}

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

3.2 volatile和synchronized的区别?

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

以上是关于java线程锁策略的主要内容,如果未能解决你的问题,请参考以下文章

带你深入理解多线程 --- 锁策略篇

java并发线程锁技术的使用

java线程锁策略

深入Java多线程锁策略

Java多线程常见面试题-第一节:锁策略CAS和Synchronized原理

Java多线程系列--“JUC锁”05之 非公平锁