初识synchronized
- 线程安全问题
- 什么是synchronized
- synchronized几种使用方式
- synchronized特性
线程安全问题
首先得知道什么是线程安全问题
这里我们打个比方
在同一个时间段我们使用两个线程对同一个数据进行++操作,这个被操作数据就可能会出现线程安全问题,假如说这个数据是0,两个线程同时++,我们想要得到的数据是2,但其实最后是1,原因是线程之间是不可见的。
第一个线程在读这个数的时候是0,假如第二个线程在第一个线程写回前读了这个数,那么它读到的也是0,这个时候第一个线程写回也就是++操作,这个时候这个数是1,而第二个线程对读到的数进行++操作,写回的时候也是1。
我们看代码:
public class test4 {
public static class MyRunna implements Runnable {
public static int count;
@Override
public void run() {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(new MyRunna()).start();
}
Thread.sleep(500);
System.out.println("循环1000次以后count为" + MyRunna.count);
}
}
执行结果为:
循环1000次以后count为996
这个数字不太好996
不过我们可以观察到它并没有++到1000
你多执行几次可以发现它每次的结果都不一样
这就是通俗的理解线程安全问题
什么是synchronized
我们刚明白了线程安全问题大致是什么意思,那么肯定得有解决办法
还是按照上面的代码为例子,这里暂且不理会写法,后面会细说
public class test4 {
public static class MyRunna implements Runnable {
public static int count;
public static Object object = new Object();
@Override
public void run() {
synchronized (object) {
count++;
}
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(new MyRunna()).start();
}
Thread.sleep(500);
System.out.println("循环1000次以后count为" + MyRunna.count);
}
}
执行结果为
循环1000次以后count为10000
我们使用了synchronized关键字,我们给操作的对象上了一把锁,这里引用一下百度知道对synchronized的解释
synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。它包括两种用法:synchronized 方法和 synchronized 块。
我们大致解释了一下synchronized是什么,下面将会解释它的一些基本使用方式和写法
synchronized几种使用方式
通过this锁定
如果每次我们都要去定义一个锁的对象,也就是上面的Object object
,每次都要new一个对象出来,那样加锁就过于繁琐了,有一个简单的方式,synchronized(this)
也就是锁定当前对象
贴一点部分代码出来
public void run() {
synchronized (this) {
count++;
}
}
锁定方法
我们也可以直接在方法上面进行上锁
public synchronized void run() {
count++;
}
这和上面的方式其实是等价的
锁定静态方法
我们知道静态方法static
是没有this对象的,你可以不用new一个对象来调用这个方法,假如这个方法上加一个synchronized就是相当于对当前类上锁,我们看代码
假如这个类名字叫T,那么直接加synchronized等价于synchronized(T.class)
public synchronized static void run() {
count++;
}
总结一下其实也就是三种方式
1.普通同步方法,锁定当前对象
2.static静态方法,锁定class类
3.同步方法块,锁定括号里的对象,对给对象加锁,进入同步代码库前要获得给定对象的锁
锁优化
一般有两种,一种是粗粒度,一种是细粒度
其实也就是锁对象和代码块,有时候一个对象里的代码并不是都需要锁,假如这个时候给整个对象上锁就不太合适,我们可以把锁细化,只锁定我们需要锁的代码块
假如说这整个对象里面的各个地方都用到锁,就没必要细化了,直接给整个对象加锁就好了
synchronized特性
synchronizd有那么几种特性
1.可重入
2.可见性
3.出现异常自动释放
可重入
什么是可重入
如果一个同步方法调用另外一个同步方法,一个方法加了锁另外一个方法也加了锁,加的是同一把锁也是同一线程,那这个时候申请仍然会得到该对象的锁。怎么解释可重入呢,比如有一个方法m1是synchronized的,有个方法m2也是synchronized的,这个时候m1方法里面是可以调用m2方法的。当一个线程调用m1的时候获得了这个把锁,然后在m1里面调用m2的时候,这个时候m2发现是同一个线程,因为m2和m1是同一个线程调用的那么允许它获得这把锁,这就是可重入。
public class test6<main> {
synchronized void m1(){
System.out.println("m1执行了");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
m2();
}
synchronized void m2(){
System.out.println("m2执行了");
}
public static void main(String[] args) {
new test6().m1();
}
}
执行结果
m1执行了
m2执行了
可见性
可见性在上面已经说过了,所以就不细说了
多线程之间对方法调用是不可见的,当线程A读一个数的时候,线程B也读了,最后A写入的时候,B有没有写入A是不知道的,这就是线程不可见,而synchronized具有可见性,用于确保写线程更新变量后,读线程再去访问的时候可以读到该变量的最新值。
上面已经有代码实现过了这里就不演示了。
出现异常自动释放锁
程序在执行过程中,如果出现异常,默认情况下锁会被释放,所以,在并发过程中异常的处理很重要,不然如果异常处理的不合适,在第一个线程中抛出异常,接下来的线程中就会进入同步代码块,那么就有可能读到异常时的数据。
我们看一下代码
public class test7 {
synchronized void erroTest() {
for (int i = 0; i < 10; i++) {
System.out.println("线程"+Thread.currentThread().getName()+"循环第" + i);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (i == 5) {
int i1 = i / 0;//此处抛出异常,锁将被释放,要想不被释放,可以在这里进行 catch,然后让循环继续
}
}
}
public static void main(String[] args) {
test7 test7 = new test7();
Runnable runnable = new Runnable() {
@Override
public void run() {
test7.erroTest();
}
};
new Thread(runnable,"r1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(runnable,"r2").start();
}
}
代码可能不是很严谨,但是并不影像我们观察这个特性
synchronized锁升级
- jdk早期的时候,synchronized的底层实现是重量级的,需要找操作系统去申请锁,效率非常低。
- 后来经过改进,有了现在synchronized锁升级的概念
锁升级经历这么几个过程
偏向锁 -> 自旋锁(轻量级锁,无锁) -> 重量级锁
然后我们依次介绍这几个锁是什么意思
偏向锁
偏向锁就是偏向第一个获得它的线程,在接下来的执行过程中,假如该锁没有被其它线程获取,那么下一次这个线程在来执行的时候就不需要在上锁,也就是说这个锁偏向第一个线程。在此线程执行过程中,中途退出或者加入并不在需要去进行加锁和解锁的操作。
这里面有个markword记录了这个线程的id,具体比较细致,后面讲
自旋(CAS)
所谓的自旋就是指当一个线程来竞争锁的时候,这个线程会在原地转圈,也就是循环,而不是直接把线程阻塞,锁在原地循环的时候是消耗cpu的,就相当于是在执行一个什么都没有的空循环(jdk1.6默认是循环10次)。这就自旋,也叫轻量级锁,也叫无锁。
重量级锁
重量级锁就是依赖于对象内部的monitor实现的,而monitor又是依赖于操作系统的MutexLock实现的,所以重量级锁也叫做互斥锁。
这里先简单介绍下一些基础概念,关于锁升级和对象头的相关基础知识请看这篇文章https://www.cnblogs.com/ccsert/p/12381817.html