Java中synchronized详解
synchronized官方解释
翻译成中文:
Synchronized同步方法可以支持使用一种简单的策略来防止线程干扰和内存一致性错误:如果一个对象对多个线程可见,则对该对象变量的所有读取或写入都是通过同步方法完成的。
简单就是说Synchronized的作用就是Java中解决并发问题的一种最常用最简单的方法 ,他可以确保同一个时刻最多只有一个线程执行同步代码,从而保证多线程环境下并发安全的效果。 如果有一段代码被Synchronized所修饰,那么这段代码就会以原子的方式执行,当多个线程在执行这段代码的时候,它们是互斥的,不会相互干扰,不会同时执行。
Synchronized工作机制是在多线程环境中使用一把锁,在第一个线程去执行的时候去获取这把锁才能执行,一旦获取就独占这把锁直到执行完毕或者在一定条件下才会去释放这把锁,在这把锁释放之前其他的线程只能阻塞等待。
synchronized是Java中的关键字,被Java原生支持,是一种最基本的同步锁。
它修饰的对象有以下几种:
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。
3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象。
4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
不使用并发手段会有什么后果
两个线程同时i++,最后结果比预计的要少。
下面是测试代码
public class sync1 implements Runnable{ static sync1 instance = new sync1(); static int i = 0; @Override public void run() { for (int j = 0; j < 100000; j++) { i++; } } public static void main(String[] args) throws InterruptedException {
// 开启两个线程 公用同一个instance实例 Thread t1 = new Thread(instance); Thread t2 = new Thread(instance); t1.start(); t2.start(); // 让线程先等待一会 先输出i t1.join(); t2.join(); System.out.println(i); } }
输出结果
每次输出的结果是不同的,并且不符合我们的预期。
原因:
i++看上去是一个操作,其实包含的是三个操作
1)读取i。
2)将i加一。
3)将i的值写入到内存中。
synchronized两种用法
1. 对象锁
包括方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己指定锁对象)。
synchronized(this|object) {}
synchronized(类.class) {}
2. 类锁
指Synchronized修饰的静态的方法或指定锁为Class对象。
对象锁
代码块形式:手动指定锁对象。
方法锁形式:Synchronized修饰的普通方法,锁对象默认为this。
public class synchronizedCode implements Runnable { static synchronizedCode instance = new synchronizedCode(); static int i = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(instance); Thread t2 = new Thread(instance); t1.start(); t2.start(); // 线程还存在 while (t1.isAlive() || t2.isAlive()) { } System.out.println("线程执行结束"); } @Override public void run() { System.out.println("我是对象锁的代码块形式,我叫"+ Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("我是对象锁的代码块形式,我叫"+ Thread.currentThread().getName()+"运行结束"); } }
可以看到同时执行,同时结束。
当使用synchronized包裹要保护的代码,让其顺序执行。
public class synchronizedCode implements Runnable { static synchronizedCode instance = new synchronizedCode(); static int i = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(instance); Thread t2 = new Thread(instance); t1.start(); t2.start(); // 线程还存在 while (t1.isAlive() || t2.isAlive()) { } System.out.println("线程执行结束"); } @Override public void run() { synchronized (this) { System.out.println("我是对象锁的代码块形式,我叫"+ Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("我是对象锁的代码块形式,我叫"+ Thread.currentThread().getName()+"运行结束"); } } }
输出结果可以看到synchronized保证了代码块是窜行执行的。
在上面 synchronized括号里面使用的是this,但有的时候情况会比较复杂,需要自定义一个锁对象,当业务比较复杂时候,可能需要不止有一个synchronized同步代码块,在存在多个synchronized情况下,它们可能需要互相配对。
修改上面的代码如下,有两个同步代码块,并且使用两个不同的锁。
public class synchronizedCode implements Runnable { static synchronizedCode instance = new synchronizedCode(); static int i = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(instance); Thread t2 = new Thread(instance); t1.start(); t2.start(); // 线程还存在 while (t1.isAlive() || t2.isAlive()) { } System.out.println("线程执行结束"); } 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运行结束"); } } }
以下输出结果,因为存在两个锁,所以线程不用等待同一把锁,在锁被释放的时候,两个线程可以分别同时获取两把锁,当lock2运行结束后,线程Thread-1就可以拿到lock2锁。
在一般复杂业务场景下,还会出现一个线程等待另外两个线程,还有线程相互之间通讯等问题。
普通方法锁
给普通方法加上synchronized修饰符。
public class objectMethod implements Runnable { static objectMethod instance = new objectMethod(); static int i = 0; @Override public void run() { // 调用 try { method(); } catch (InterruptedException e) { e.printStackTrace(); } } public synchronized void method() throws InterruptedException { System.out.println("该方法是对象锁的方法修饰符形式, 线程名为:"+ Thread.currentThread().getName()); Thread.sleep(3000); System.out.println(Thread.currentThread().getName()+"运行结束"); } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(instance); Thread t2 = new Thread(instance); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("运行结束"); } }
运行结果
上面代码两个线程调用Method方法,根据结果可知,依然是按照顺序执行。
类锁
我们知道Java类可能会有多个对象,但是只有一个Class对象。
查看线程生命周期
1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3. 阻塞(BLOCKED):表示线程阻塞于锁。
4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
6. 终止(TERMINATED):表示该线程已经执行完毕。