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种应用方式,下面分别介绍
-
修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
-
修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
-
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁
- 修饰实例方法:使用synchronized修饰了increase方法,new了两个不同的实例对象时就有两个不同的锁
public synchronized void increase(){
i++;
}
- 修饰静态方法:当synchronized作用于静态方法时,其锁就是当前类的class对象锁。
public static synchronized void increase(){
i++;
}
- 修饰代码块:将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线程锁策略的主要内容,如果未能解决你的问题,请参考以下文章