锁是什么?什么要使用它?
相信接触过并发的人都知道synchronized是一种锁,那什么是锁呢?显示生活中的解释:一把锁管控这某一个空间,只有人们拿钥匙打开锁才能进入。编程世界中的锁,和现实世界很相似,它的目的很明确:加锁的方法或者代码块,只有拿到钥匙才能访问。
那什么时候需要用到它呢?这个就要看业务场景,如果程序没有牵扯到并发或者共享变量不存在竞争,这种情况下一般不需要使用到锁,但是现在的程序基本上是不可能不存在资源竞争的,这时候就要在高性能和高一致性作取舍,选择高可用,那么一致性就会下降,如果保证强一致性,那么性能就会收到瓶颈,所以,锁是一把双刃剑,用好他可以让程序快乐的飞起,用不好,可能造成不可估量的后果。
synchronized是什么?
synchronized其实就是一把锁,它能保证数据的一致性,但是会牺牲一部分的性能。
优点:
1.保证线程互斥
2.保证数据一致性
缺点:
1.效率较低
2.使用不当容易造成死锁(程序崩溃)
所以锁是一把双刃剑,用好它,才是一个合格的程序猿。
synchronized的四种用法
1.锁定普通的方法
2.锁定静态方法
3.锁定当前类
4.锁定代码块
介绍这4中用法之前我们先看一下这个程序:
package com.ymy.utils;
/**
* synchronized锁
*/
public class MySynchronized {
public static long sum = 0;
public static void add(){
for(long i = 0 ;i <10000 ; i++){
// count.incrementAndGet();
sum += 1;
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
add();
});
t1.start();
Thread t2 = new Thread(() ->{
add();
});
t2.start();
t1.join();
t2.join();
System.out.println("累加总和:"+sum);
}
}
程序很简单,两个线程都让sum累加1w,我们预期的结果应该是2w,结果是多少呢?程序不能凭我们想象,因该要去实践,运行结果如下:
第一次:
累加总和:17854
第二次:
累加总和:16268
第三次:
累加总和:20000
为什么会出现这种结果呢?那这个就要说到cpu和内存了,想必大家都知道计算机cpu处理速度远远大于内存,由于内存的速度相对于cpu来说较慢,就会导致cpu在很长的一段时间都处于空闲状态,这样大大浪费了cpu的性能,后来就有大牛研在cpu上做了一个缓存配置,也叫cpu缓存,那现在程序是如何工作的呢?
这中操作为什么为造成上面的输出结果呢?如果对多线程有点了解的话,应该知道,线程之间的执行是无序的,cpu会根据线程的优先级来处理线程,如过线程a得到了执行权,但是cpu不会一直执行线程,cpu会在多个线程之间来回切换,处理不同的事情,这就是为什么你在使用qq向A发送视频的时候还能和B聊天,如果没有这种线程切换,那么你只能等视频发送完成,你才能和B交流,线程(上下文)切换就是导致上面输出结果不等于2w的原因。
为什么呢?如果线程1先执行第一次读取内存数据sum的时候等于0,将值读到cpu缓存之后还没来得及做自加操作,此时切换到了线程2,他往内存读数据的时候也是0,因为线程1还没有进行计算,他们计算完的结果都是1,然后在分别写回内存,这就导致了原本结果应该是2却得到了1,为了解决这个并发问题,锁就出现了。
下面我们来介绍一下synchronized的四种用法。
锁定普通的方法
什么叫锁定普通方法呢?
就是在add()前面加上synchronized关键字,锁住的区域也仅仅是当前实例,代码如下:
package com.ymy.utils;
/**
* synchronized锁
*/
public class MySynchronized {
public static long sum = 0;
public synchronized void add(){
for(long i = 0 ;i <10000 ; i++){
sum += 1;
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
new MySynchronized().add();
});
t1.start();
Thread t2 = new Thread(() ->{
new MySynchronized().add();
});
t2.start();
t1.join();
t2.join();
System.out.println("累加总和:"+sum);
}
}
需要注意的一点是这样加锁是不能保证线程安全的,因为普通方法加锁只能锁住当前实例,而我在main方法中实例化了2个MySynchronized对象,所以这里有两把锁,分别锁着两个不同的实例,这两个线程之间还是存在问题,如果保证线程安全,只需要做一下改动,让两个线程共用一个实例即可,代码如下:
package com.ymy.utils;
/**
* synchronized锁
*/
public class MySynchronized {
public static long sum = 0;
public synchronized void add(){
for(long i = 0 ;i <10000 ; i++){
sum += 1;
}
}
public static void main(String[] args) throws InterruptedException {
MySynchronized mySynchronized = new MySynchronized();
Thread t1 = new Thread(() -> {
mySynchronized.add();
});
t1.start();
Thread t2 = new Thread(() ->{
mySynchronized.add();
});
t2.start();
t1.join();
t2.join();
System.out.println("累加总和:"+sum);
}
}
修改前结果:
修改后结果:
锁定静态方法
什么叫锁定静态方法呢?其实就是在方法前加一个static关键字,这种方式锁定的是当前类,不管你创建多少实例,它都能保证前程的互斥性。
package com.ymy.utils;
/**
* synchronized锁
*/
public class MySynchronized {
public static long sum = 0;
public static synchronized void add(){
for(long i = 0 ;i <10000 ; i++){
sum += 1;
}
}
public static void main(String[] args) throws InterruptedException {
MySynchronized mySynchronized = new MySynchronized();
Thread t1 = new Thread(() -> {
new MySynchronized().add();
});
t1.start();
Thread t2 = new Thread(() ->{
new MySynchronized().add();
});
t2.start();
t1.join();
t2.join();
System.out.println("累加总和:"+sum);
}
}
由于这种加锁方式是锁定当前类,虽然保证了所有线程的互斥性,但是性能极低,使用的还需慎重。
代码块锁定(锁定当前类)
刚刚介绍加锁带static关键字的方法就是锁定当前类,为什么这里还要介绍一下锁定当前类呢?那是因为通过锁定static方法只是其中的一种方式,还可以通过代码块来锁定当前类,请看代码:
package com.ymy.utils;
/**
* synchronized锁
*/
public class MySynchronized {
public static long sum = 0;
public void add() {
synchronized (MySynchronized.class) {
for (long i = 0; i < 10000; i++) {
sum += 1;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
new MySynchronized().add();
});
t1.start();
Thread t2 = new Thread(() -> {
new MySynchronized().add();
});
t2.start();
t1.join();
t2.join();
System.out.println("累加总和:" + sum);
}
}
这种方式就是通过代码块锁定当前类,效果和锁定static方法是一样的,这样做的效果会比第一种好一点,为什么这么说呢?因为static锁定的是整个方法,而代码块锁定的是你需要锁定的某一行或者某几行代码,效果比较好。
代码块锁定(对象)
锁定一个你指定的对象,所有引用此对象的线程保持同步(互斥),不同对象互不影响。
package com.ymy.utils;
/**
* synchronized锁
*/
public class MySynchronized {
public static long sum = 0;
private static String lock1 = "lock1";
private static String lock2 = "lock2";
public void add() throws InterruptedException {
synchronized (lock1) {
System.out.println("这里锁住的对象是:lock1,线程名:"+Thread.currentThread().getName());
Thread.sleep(5000);
System.out.println("lock1休眠结束");
}
synchronized (lock2) {
System.out.println("这里锁住的对象是:lock2,线程名:"+Thread.currentThread().getName());
Thread.sleep(5000);
System.out.println("lock2休眠结束");
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
new MySynchronized().add();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread t2 = new Thread(() -> {
try {
new MySynchronized().add();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t2.start();
}
}
输出结果:
lock1和lock2处于并行状态,为什么第一次进来锁定lock1的时候lock2处于阻塞状态呢?那是因为add()是一个同步方法,代码忧伤到下依次执行。
在这里还要注意一点:synchronized的可重入行以及不可中断性