作者某人Valar
如需转载请保留原文链接
目录:
synchronized中文意为:同步的,同步化的。是Java中的一个关键字。
常用作给 方法 或者 代码块 加锁。加锁后,同一时刻只能有一个线程执行这段代码。以此来保证线程安全。
先简单理解下3个概念,
原子(atom)本意指化学反应不可再分的基本微粒。
为什么程序的执行顺序有时会不按照代码的先后顺序执行呢?
这里面涉及到 指令重排序 (Instruction Reorder)的概念。在Java内存模型中,允许编译器和处理器对指令进行重排序,重排序的结果不会影响到 单线程 的执行,但 不能保证多线程并发执行 时不受影响。
例如以下代码在未发生指令重排序时,其执行顺序为1->2->3->4。但在真正执行时,将可能变为1->2->4->3或者2->1->3->4或者其他。但其会保证1处于3之前,2处于4之前。所有最终结果都是 a=10; b=20
。
int a = 0;//语句1 int b = 1;//语句2 a = 10; //语句3 b = 20; //语句4 复制代码
但如果是多线程情况下,另一个线程中有以下程序。当上述的执行顺序被重排序为1->2->4->3,当线程1执行到第3步 b=20
时,切换到线程2执行,其会输出 a此时已经是10了
,而此时a的值其实还是为0。
if(b == 20){ System.out.print("a此时已经是10了"); } 复制代码
被synchronized关键字包裹起来的方法或者代码块可以认为是原子的。因为在锁未释放之前,这段代码无法被其他线程访问到,所以从一个线程观察另外一个线程的时候,看到的都是一个个原子性的操作。
在Java中,synchronized对应着两个字节码指令 monitorenter
和 monitorexit
。通过 monitorenter
和 monitorexit
指令,可以保证被synchronized修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。
根据JMM(Java Memory Model,Java内存模型)机制,内存主要分为主内存和工作内存两种,线程工作时会从主内存中拷贝一份变量到工作内存中。
JMM对synchronized做了2条规定:
synchronized可以保证一定程度的有序性,但其是不能禁止指令重排序的,synchronized 代码块里的非原子操作依旧可能发生指令重排。
具体怎么理解呢?
as-if-serial语义
,其是指不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守 as-if-serial语义
。 as-if-serial语义
保证了单线程中指令重排序是有一些限制的,即无论怎么重排序,都不能影响到单线程执行的结果。而synchronized保证了这一块程序在同一时间内只能被同一线程访问,所以其也算是保证了有序性。 synchronized的用法大概可以分为3种,
其作用在方法上的写法如下图, synchronized
只要放在 返回类型
前面就行。
下面将举一些具体的实例,来看下使用 synchronized
后的结果:
public class TestBean { //TestBean中有两个实例方法,method1和method2 public synchronized void method1(){ System.out.println("method1 start"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("method1 end"); } public synchronized void method2(){ System.out.println("method2 start"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("method2 end"); } } 复制代码
public class MainTest { public static void main(String[] args){ TestBean testBean = new TestBean(); //第一个线程,执行method1() new Thread(new Runnable() { @Override public void run() { testBean.method1(); } }).start(); /立刻开启第二个线程,执行method2() new Thread(new Runnable() { @Override public void run() { testBean.method2(); } }).start(); } } 复制代码
控制台结果:
method1 start (...3秒后输出) method1 end method2 start (...3秒后输出) method2 end 复制代码
可以看出锁作用于 testBean
对象上,其他线程来访问 synchronized
修饰的其他方法时需要等待线程1先把锁释放。
method2()
的 synchronized
修饰符去掉呢。那自然是不用等锁释放,就会立刻执行 method2()
。控制台输出以下结果: method1 start method2 start (...3秒后输出) method1 end method2 end 复制代码
synchronized
修饰,但有两个不同的实例对象。 public class MainTest { public static void main(String[] args){ TestBean testBean1 = new TestBean(); TestBean testBean2 = new TestBean(); //第一个线程,执行method1() new Thread(new Runnable() { @Override public void run() { testBean1.method1(); } }).start(); /立刻开启第二个线程,执行method2() new Thread(new Runnable() { @Override public void run() { testBean2.method2(); } }).start(); } } 复制代码
此时因为两个线程作用于不同的对象,获得的是不同的锁,所以互相并不影响,控制台的结果如下:
method1 start method2 start (...3秒后输出) method1 end method2 end 复制代码
将上文中的TestBean.java改为:
public class TestBean { //TestBean中有一个静态方法,method synchronized public static void method(String threadName){ System.out.println("method start by " + threadName); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("method end by " + threadName); } } 复制代码
Main.java如下:
public class MainTest { public static void main(String[] args){ TestBean testBean1 = new TestBean(); TestBean testBean2 = new TestBean(); new Thread(new Runnable() { @Override public void run() { testBean1.method("thread1"); } }).start(); new Thread(new Runnable() { @Override public void run() { testBean2.method("thread2"); } }).start(); } } 复制代码
控制台结果:
method start by thread1 (...3秒后输出) method end by thread1 method start by thread2 (...3秒后输出) method end by thread2 复制代码
分析:由例子可知,两个线程虽然使用的是两个不同的对象,但是访问的方法是静态的,两个线程最终还是发生了互斥(即一个线程访问,另一个线程只能等着),因为静态方法是依附于类而不是对象的,当synchronized修饰静态方法时,锁是class对象。
为什么要作用于代码块呢?
在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要 同步的代码又只有一小部分 ,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,毕竟长锁不如短锁,尽可能只锁必要的部分。
public class TestBean { private final static Object objectLock = new Object(); void method1(){ System.out.println("not synchronized method1"); synchronized (objectLock){ System.out.println("synchronized method1 start"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("synchronized method1 end "); } } void method2(){ System.out.println("not synchronized method2" ); synchronized (objectLock){ System.out.println("synchronized method2 start"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("synchronized method2 end"); } } } 复制代码
public class MainTest { public static void main(String[] args){ TestBean testBean1 = new TestBean(); TestBean testBean2 = new TestBean(); new Thread(new Runnable() { @Override public void run() { testBean1.method1(); } }).start(); new Thread(new Runnable() { @Override public void run() { testBean2.method2(); } }).start(); } } 复制代码
控制台输出:
not synchronized method1 synchronized method1 start not synchronized method2 (...3秒后输出) synchronized method1 end synchronized method2 start (...3秒后输出) synchronized method2 end 复制代码
可以看到未被synchronized包裹的代码时不存在互斥的, System.out.println("not synchronized method2" );
无需等到method1执行完成,而被synchronized包裹的代码块,且使用了同一个对象作为锁的话,那就互斥了。
结论:
synchronized
作用于一个给定的实例对象objectLock,每次当线程进入 synchronized
包裹的代码块时就会要求当前线程持有objectLock实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待。 this
对象(代表当前实例)或者当前类的 class对象
作为锁,如下代码: synchronized (this){ System.out.println("synchronized method1 start"); System.out.println("synchronized method1 end"); } synchronized (TestBean.class){ System.out.println("synchronized method2 start"); System.out.println("synchronized method2 end"); } 复制代码
Lock
是Java语言中的一个接口类,对应的实现类为 ReentrantLock
,它们都位于 java.util.concurrent.locks
包下,熟悉Java的同学肯定都知道concurrent包下都是用于处理Java多线程问题的类。
lock的几个常用方法:
lock():获取锁,如果锁被暂用则一直等待 unlock():释放锁 tryLock(): 注意返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回true tryLock(long time, TimeUnit unit):比起tryLock()就是给了一个时间期限,保证等待参数时间 ck lockInterruptibly():用该锁的获得方式,如果线程在获取锁的阶段进入了等待,那么可以中断此线程,先去做别的事 复制代码
lock的使用与synchronized作用于代码块时类似:
public class TestBean { private Lock lock = new ReentrantLock(); void method1(String threadName) { lock.lock(); try{ System.out.println("method1 start " + threadName); //耗时操作 ... } finally { System.out.println("method1 end " + threadName); lock.unlock();//释放锁 } } } 复制代码
public class MainTest { public static void main(String[] args){ TestBean testBean1 = new TestBean(); new Thread("thread1") { @Override public void run() { testBean1.method1(Thread.currentThread().getName()); } }.start(); new Thread("thread2"){ @Override public void run() { testBean1.method1(Thread.currentThread().getName()); } }.start(); } } 复制代码
执行结果如下:
method1 start thread1 (...3秒后输出) method1 end thread1 method1 start thread2 (...3秒后输出) method1 end thread2 复制代码
注意:使用lock时,需要在finally中释放锁 lock.unlock();
,不然可能会造成死锁。
tryLock()
方法的使用: public class TestBean { private Lock lock = new ReentrantLock(); void method1(String threadName) { if (lock.tryLock()){ try{ System.out.println("method1 start " + threadName); //耗时操作 ... } finally { lock.unlock(); } } else { System.out.println("我是"+threadName+",有人占着锁,我放弃了"); } } } 复制代码
控制台输出:
method1 start thread1 我是thread2,有人占着锁,我放弃了 (...3秒后输出) method1 end thread1 复制代码