Synchronized的作用
简单来说:就是能够保证在同一时刻最多只有一个线程执行该段代码,已达到保证并发安全的效果。
地位:
1、Synchronized是Java的关键字,被Java语言原生支持
2、是最基本的互斥同步手段
3、是并发编程中的元老级角色,是并发编程中必学内容
不使用Synchronized带来的后果
示例:两个线程同时操作一个变量,实现不断累加的效果。
public class synchronizedTest implements Runnable {
static int i = 0;
@Override
public void run() {
System.out.println("name:" + Thread.currentThread().getName());
for (int j = 0; j < 100000; j++) {
i++;
}
}
public static void main(String[] args) throws InterruptedException {
synchronizedTest synchronizedTest = new synchronizedTest();
Thread thread1 = new Thread(synchronizedTest);
Thread thread2 = new Thread(synchronizedTest);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(i);
}
}
这里使用join方法,是为了让一个线程进入一个等待的状态,等该线程执行完之后,再去执行下一行代码。
我们预期的结果应该是200000,但事实并非如此。执行结果:
name:Thread-1
name:Thread-0
127096
每次运行结果都会不同,显然这是不安全的。
原因:
i++看上去只有一行代码,其实包括三个步骤:
1)读取i的值
2)将i进行加1
3)最后将i的值写入到内存中
如果线程1刚执行到第二步骤,此时线程2开始读取i的值,显然线程1还没有完成对i进行加1的操作,这样两个线程对i的操作就乱套了。
Synchronized的两种用法
1、对象锁
包括方法锁(修饰普通方法,默认锁对象是this)和同步代码块锁(自己指定锁对象)
2、类锁
指的是synchronized修饰的静态方法或指定锁为class对象。类锁只能在同一时刻被一个对象拥有。
我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁互不干扰,但是每个类只有一个类锁。有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的
一、对象锁
1、this对象锁的使用
public class synchronizedTest implements Runnable {
@Override
public void run() {
synchronized (this){
System.out.println("对象锁代码,线程名字:" + Thread.currentThread().getName());
//这里加了3秒延迟,是为了更清楚看到线程的执行顺序
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( Thread.currentThread().getName() + "执行完毕");
}
}
public static void main(String[] args) {
synchronizedTest synchronizedTest = new synchronizedTest();
Thread thread1 = new Thread(synchronizedTest);
Thread thread2 = new Thread(synchronizedTest);
thread1.start();
thread2.start();
while (thread1.isAlive() || thread2.isAlive()){
//保证线程执行完毕
}
System.out.println("finished");
}
}
执行结果
对象锁代码,线程名字:Thread-0
Thread-0执行完毕
对象锁代码,线程名字:Thread-1
Thread-1执行完毕
finished
加了synchronized修饰,线程是串行执行,解决了并发的问题。
2、自己创建对象锁
public class synchronizedTest implements Runnable {
Object lock1 = new Object();
Object lock2 = new Object();
@Override
public void run() {
synchronized (lock1){
System.out.println("我是lock1,线程名字:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( Thread.currentThread().getName() + ":lock1执行完毕");
}
synchronized (lock2){
System.out.println("我是lock2,线程名字:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( Thread.currentThread().getName() + ":lock2执行完毕");
}
}
public static void main(String[] args) {
synchronizedTest synchronizedTest = new synchronizedTest();
Thread thread1 = new Thread(synchronizedTest);
Thread thread2 = new Thread(synchronizedTest);
thread1.start();
thread2.start();
while (thread1.isAlive() || thread2.isAlive()){
//保证线程执行完毕
}
System.out.println("finished");
}
}
执行结果
我是lock1,线程名字:Thread-0
Thread-0:lock1执行完毕
我是lock2,线程名字:Thread-0
我是lock1,线程名字:Thread-1
Thread-0:lock2执行完毕
Thread-1:lock1执行完毕
我是lock2,线程名字:Thread-1
Thread-1:lock2执行完毕
finished
但Thread-0执行完第一个同步代码块的时候,同时Thread-1会立即获取该对象锁。
Thread-0:lock1执行完毕
我是lock2,线程名字:Thread-0
我是lock1,线程名字:Thread-1
这三行日志,几乎是并行执行的。
Thread-0:lock2执行完毕
Thread-1:lock1执行完毕
我是lock2,线程名字:Thread-1
几乎也是并行执行完毕的。
如果使用同一把对象锁的情况
执行结果
我是lock1,线程名字:Thread-0
Thread-0:lock1执行完毕
我是lock2,线程名字:Thread-0
Thread-0:lock2执行完毕
我是lock1,线程名字:Thread-1
Thread-1:lock1执行完毕
我是lock2,线程名字:Thread-1
Thread-1:lock2执行完毕
finished
只有当Thread-0执行完之后,才释放lock1对象锁,Thread-1开始执行。
3、对象锁修饰普通方法的情况(方法锁)
public class synchronizedTest implements Runnable {
@Override
public void run() {
method();
}
private synchronized void method() {
System.out.println("我是方法锁,线程名字:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( Thread.currentThread().getName() + "执行完毕");
}
public static void main(String[] args) {
synchronizedTest synchronizedTest = new synchronizedTest();
Thread thread1 = new Thread(synchronizedTest);
Thread thread2 = new Thread(synchronizedTest);
thread1.start();
thread2.start();
while (thread1.isAlive() || thread2.isAlive()){
//保证线程执行完毕
}
System.out.println("finished");
}
}
执行结果:
我是方法锁,线程名字:Thread-0
Thread-0执行完毕
我是方法锁,线程名字:Thread-1
Thread-1执行完毕
finished
实现了串行执行的效果。
二、类锁
1、synchronized用在static修饰的方法上
public class synchronizedTest implements Runnable {
@Override
public void run() {
method();
}
private static synchronized void method() {
System.out.println("使用static修饰的情况,线程名字:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( Thread.currentThread().getName() + "执行完毕");
}
public static void main(String[] args) {
synchronizedTest synchronizedTest1 = new synchronizedTest();
synchronizedTest synchronizedTest2 = new synchronizedTest();
Thread thread1 = new Thread(synchronizedTest1);
Thread thread2 = new Thread(synchronizedTest2);
thread1.start();
thread2.start();
while (thread1.isAlive() || thread2.isAlive()){
//保证线程执行完毕
}
System.out.println("finished");
}
}
如果我们不使用static修饰方法,执行结果
不使用static修饰的情况,线程名字:Thread-0
不使用static修饰的情况,线程名字:Thread-1
Thread-1执行完毕
Thread-0执行完毕
finished
由于创建了不同的实例对象,两个线程拿到的对象锁也不相同,它们可以同时访问synchronized修饰的普通方法,两个线程并行执行,这时synchronized失去了它的作用。
使用static修饰方法,执行结果
使用static修饰的情况,线程名字:Thread-0
Thread-0执行完毕
使用static修饰的情况,线程名字:Thread-1
Thread-1执行完毕
finished
可见,两个线程实现了串行执行。
2、synchronized(*.class)代码块
public class synchronizedTest implements Runnable {
@Override
public void run() {
method();
}
private void method() {
synchronized (synchronizedTest.class){
System.out.println("使用class对象的情况,线程名字:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( Thread.currentThread().getName() + "执行完毕");
}
}
public static void main(String[] args) {
synchronizedTest synchronizedTest1 = new synchronizedTest();
synchronizedTest synchronizedTest2 = new synchronizedTest();
Thread thread1 = new Thread(synchronizedTest1);
Thread thread2 = new Thread(synchronizedTest2);
thread1.start();
thread2.start();
while (thread1.isAlive() || thread2.isAlive()){
//保证线程执行完毕
}
System.out.println("finished");
}
}
执行结果
使用class对象的情况,线程名字:Thread-0
Thread-0执行完毕
使用class对象的情况,线程名字:Thread-1
Thread-1执行完毕
finished
多线程访问同步方法的7种情况
1、两个线程同时访问一个对象的同步方法
答:串行执行
2、两个线程访问的是两个对象的同步方法
答:并行执行,因为两个线程持有的是各自的对象锁,互补影响。
3、两个线程访问的是synchronized的static方法
答:串行执行,持有一个类锁
4、同时访问同步方法和非同步方法
答:并行执行,无论是同一对象还是不同对象,普通方法都不会受到影响
5、访问同一对象的不同的普通同步方法
答:串行执行,持有相同的锁对象
6、同时访问静态的synchronized方法和非静态的synchronized方法
答:并行执行,因为一个是持有的class类锁,一个是持有的是this对象锁,不同的锁,互补干扰。
7、方法抛出异常后,会释放锁
答:synchronized无论是正常结束还是抛出异常后,都会释放锁,而lock必须手动释放锁才可以。
问题3和4参考代码:
public class synchronizedTest implements Runnable {
@Override
public void run() {
if (Thread.currentThread().getName().equals("Thread-0")) {
method1();
} else {
method2();
}
}
private synchronized void method1() {
System.out.println("使用synchronized修饰的方法,线程名字:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行完毕");
}
private void method2() {
System.out.println("使用普通方法,线程名字:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行完毕");
}
public static void main(String[] args) {
synchronizedTest synchronizedTest1 = new synchronizedTest();
Thread thread1 = new Thread(synchronizedTest1);
Thread thread2 = new Thread(synchronizedTest1);
thread1.start();
thread2.start();
while (thread1.isAlive() || thread2.isAlive()) {
//保证线程执行完毕
}
System.out.println("finished");
}
}
执行结果
使用synchronized修饰的方法,线程名字:Thread-0
使用普通方法,线程名字:Thread-1
Thread-1执行完毕
Thread-0执行完毕
finished
synchronized的缺陷
当某个线程进入同步方法获得对象锁,那么其他线程访问这里对象的同步方法时,必须等待或者阻塞,这对高并发的系统是致命的,这很容易导致系统的崩溃。如果某个线程在同步方法里面发生了死循环,那么它就永远不会释放这个对象锁,那么其他线程就要永远的等待。这是一个致命的问题。
思考:既然有了synchronized修饰方法的同步方式,为什么还需要synchronized修饰同步代码块的方式呢?
而这个问题也是synchronized的缺陷所在。当然同步方法和同步代码块都会有这样的缺陷,只要用了synchronized关键字就会有这样的风险和缺陷。既然避免不了这种缺陷,那么就应该将风险降到最低。这也是同步代码块在某种情况下要优于同步方法的方面,因为synchronized修饰同步代码块的方式比修饰方法的方式粒度小。
synchronized的性质
1、可重入性:指的是同一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁。获得锁之后不需要重新获取,可直接获取,直到自己释放。
好处:避免死锁,提升封装性(如果不可重入,假设method1拿到锁之后,在method1中又调用了method2,如果method2没办法使用method1拿到的锁,那method2将一直等待,但是method1由于未执行完毕,又无法释放锁,就导致了死锁,可重入正好避免了这种情况)
粒度:线程而非调用
这里解释下粒度:指的就是锁的作用范围,锁定的粒度越小(即锁定的业务代码越少),效率越高。这里Java的关键字synchronized加锁的范围默认指的是线程。而其他不一定是,例如Linux里的pthread线程,它是以调用为粒度的。
通过以下三种情况证明可重入性
(1)同一个方法是可重入的(递归调用)
public class synchronizedTest {
int i = 0;
private synchronized void method1() {
System.out.println("我是method1,i=" + i);
if (i == 0) {
i++;
method1();
}
}
public static void main(String[] args) {
new synchronizedTest().method1();
}
}
执行结果
我是method1,i=0
我是method1,i=1
(2)可重入不要求是同一个方法
public class synchronizedTest {
private synchronized void method1() {
System.out.println("我是method1");
method2();
}
private synchronized void method2() {
System.out.println("我是method2");
}
public static void main(String[] args) {
new synchronizedTest().method1();
}
}
执行结果
我是method1
我是method2
(3)可重入不要求是同一个类中
public class synchronizedTest {
public void superMethod() {
System.out.println("我是父类方法");
}
}
class ChildClass extends synchronizedTest {
@Override
public void superMethod() {
System.out.println("我是子类方法");
super.superMethod();
}
public static void main(String[] args) {
new ChildClass().superMethod();
}
}
执行结果
我是子类方法
我是父类方法
通过以上三种情况测试,可知synchronized的粒度是线程范围,而不是调用。
2、不可中断
如果一个锁已经被一个线程获得,其他线程还想获得,只能选择等待或者阻塞,直到正在用的线程释放这个锁,其他线程才能获得,否则永远等待。(Lock可以中断)
synchronized和Lock的区别
1、Lock是java的一个interface接口,而synchronized是Java中的关键字,synchronized是由JDK实现的,不需要程序员编写代码去控制加锁和释放。
2、synchronized修饰的代码在执行异常时,jdk会自动释放线程占有的锁,不需要程序员去控制释放锁,因此不会导致死锁现象发生;但是,当Lock发生异常时,如果程序没有通过unLock()去释放锁,则很可能造成死锁现象,因此Lock一般都是在finally块中释放锁;格式如下:
Lock lock = new ReentrantLock();
lock.lock();//加锁
try{
//TODO
}catch (Exception e){
//TODO
}finally {
lock.unlock();//释放锁
}
3、Lock可以让等待锁的线程响应中断处理,如tryLock(long time, TimeUnit unit),而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够中断,程序员无法控制.
4、通过Lock可以知道有没有成功获取锁,tryLock()方法返回boolean值,因此可知道是否获得了锁,而synchronized却无法办到。
5、Lock的实现类ReentrantReadWriteLock提供了readLock()和writeLock()用来获取读锁和写锁的两个方法,这样多个线程可以进行同时读写操作。
总体来讲,当并发量较小,资源竞争不激烈时,两者的性能是差不多的;当大量线程同时竞争,资源非常有限时,此时Lock的性能要远远优于synchronized。
总结
1、synchronized修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2、synchronized修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3、synchronized修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4、synchronized修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象。