并发编程之 Synchronized
Synchronized 关键字
- synchronized 关键字锁定的是对象不是代码块, demo 中锁的是 object 对象的实例
- 锁定的对象有两种:1.类的实例 2.类对象(类锁)
- 加 synchronized 关键字之后不一定能实现线程安全,具体还要看锁定的对象是否唯一。
锁定某个对象
public class Demo1 {
private int count = 10;
private Object object = new Object();
public void test(){
synchronized (object){
count--;
log.debug(Thread.currentThread().getName() + " count = " + count);
}
}
}
锁定当前类的实例
public class Demo2 {
private int count = 10;
public void test() {
//synchronized(this) 锁定的是当前类的实例, 这里锁定的是 Demo2 类的实例
synchronized (this) {
count--;
log.debug(Thread.currentThread().getName() + " count = " + count);
}
}
//直接加在方法声明上,相当于是上面的 synchronized(this)
public synchronized void test2() {
count--;
log.debug(Thread.currentThread().getName() + " count = " + count);
}
}
锁定当前类的 class 对象(注意区别 synchronized(this),两者不是一个概念)
public class Demo4 {
private static int count = 10;
//synchronize关键字修饰静态方法锁定的是类的对象
public synchronized static void test(){
count--;
log.debug(Thread.currentThread().getName() + " count = " + count);
}
public static void test2(){
//这里不能替换成this,不是一个概念,相当于上面加在静态方法上的 synchronized 一样
synchronized (Demo4.class){
count--;
}
}
}
锁对象的改变
- 锁定某对象 o,如果 o 的属性发生改变,不影响锁的使用
- 但是如果 o 变成另外一个对象,则锁定的对象发生改变
- 应该避免将锁定对象的引用变成另外一个对象
public class Demo1 {
Object o = new Object();
public void test(){
synchronized (o) {
//t1 在这里无限执行
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(Thread.currentThread().getName());
}
}
}
public static void main(String[] args) {
Demo1 demo = new Demo1();
new Thread(demo::test, "t1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread t2 = new Thread(demo::test, "t2");
// 这里发生了锁对象的改变,锁变成了一个新的对象
demo.o = new Object();
//t2能否执行? 答案是可以执行,因为发生了锁对象的改变。
// 假如没有上面一行的代码,那么按照逻辑,t1 会一直持有锁,并且无限执行下去,因此代码一直阻塞在 t1 处,t2 要想执行,必须等待 t1 释放锁才行,因此 t2 不会被执行。
// 然而,当锁对象发生了改变,t1 依然持有原来的锁对象,但此时 t2 已经不需要原来的 t1 持有的锁对象了,而变成了一个新的锁对象,此时,t1 和 t2 分别持有各自不同的锁对象,因而,t1 不会再阻塞 t2,换言之,t2 无需跟 t1 竞争同一把锁,t2 已经可以拿到自己独有的锁对象了,因而,t2 会被执行。
t2.start();
}
}
不要以字符串常量作为锁定的对象
下面的代码会先输出 “t1 start...”,然后休眠 5 秒,继续输出 “t1 end...”,再然后输出 “t2 start...”
定义了两个 String 对象作为锁,按道理说,t1 和 t2 应该互不影响才是,可结果却是 t1 会 阻塞 t2 线程,为什么呢?
因为定义的两个 String 对象的内容是一样的,由于 Java 中字符串常量池的存在,实际上,这两个字符串对象指向的是同一个字符串对象的引用,因此,实际上,t1 和 t2 的锁对象是同一个对象,故而他们需要竞争同一把锁,进而 t2 必须等待 t1 释放锁,t2 才能继续执行。
因此,在开发过程中要避免使用字符串常量作为锁定的对象。因为很可能你写的代码跟同事写的虽然是不同业务的代码,但都使用了字符串锁,加的锁都是同一个字符串内容,由于字符串常量池的存在,实际上锁的是同一个对象,进而引起不必要的编程问题。
public class Demo2 {
String s1 = "hello";
String s2 = "hello";
public void test1(){
synchronized (s1) {
log.debug("t1 start...");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1 end...");
}
}
public void test2(){
synchronized (s2) {
log.debug("t2 start...");
}
}
public static void main(String[] args) {
Demo2 demo = new Demo2();
new Thread(demo :: test1,"t1").start();
new Thread(demo :: test2,"t2").start();
}
}
同步代码快中的语句越少越好
- 比较 test1 和 test2
- 业务逻辑中只有 count++ 这句需要 synchronized,这时不应该给整个方法上锁
- 采用细粒度的锁,可以使线程争用时间变短,从而提高效率
public class Demo3 {
int count = 0;
public synchronized void test1(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
count ++;
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 局部加锁
*/
public void test2(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this) {
count ++;
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
同步方法和非同步方法是否可以同时调用?
答案是可以。
简单想一下,如果两个线程持有的是不同的两个锁对象,那么这两个线程不需要竞争锁,没有影响,他们都可以同时调用,那么非同步方法就更不影响了。
脏读问题
如果问你,有一个 List 集合容器,一般情况下,在插入数据时,会考虑加锁来避免多线程安全问题,那么在读取数据时,需不需要加锁呢?
答案是:实际业务当中应该看是否允许脏读,不允许的情况下对读方法也要加锁。
重入锁——方法调用
一个同步方法调用另外一个同步方法,能否得到锁? 可以
synchronized 默认支持重入。下面这个示例就是一种重入锁,test2 方法是可以拿到锁的。
public class Demo {
synchronized void test1() {
log.debug("test1 start.........");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test2();
}
/**
* 为什么test2还需要加 synchronized?他本身就包含在 test1 中,而 test1 已经加了 synchronized。
* 答案是:为了避免外面的类直接调用 test2 方法而可能产生的多线程安全问题。
*/
synchronized void test2(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("test2 start.......");
}
public static void main(String[] args) {
Demo demo= new Demo();
demo.test1();
}
}
重入锁——类继承
如下所示,类的继承,虽然表面上看起来锁的都是父子类各自的 this 对象,而真正的 this 对象指向的都是 Demo2 new 出来的实例对象,因此,这两个方法使用的是同一把锁,又发生了方法调用,因此产生了锁的重入。
public class Demo {
synchronized void test(){
log.debug("demo test start........");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("demo test end........");
}
public static void main(String[] args) {
new Demo2().test();
}
}
class Demo2 extends Demo {
@Override
synchronized void test(){
log.debug("demo2 test start........");
super.test();
log.debug("demo2 test end........");
}
}
synchronized 和异常的关系
当线程发生异常时,如果异常被捕获了,那么视为程序运行没有问题,不会因此而释放锁。
但如果异常没有被捕获,异常抛出,这时候会释放锁。这一点从 java 运行时汇编指令也可以看出,当某条执行执行出错时,计算机执行指令会直接执行释放锁的指令。
volatile 关键字,使一个变量在多个线程间可见
- main, t1 线程都用到一个变量,java 默认是 T1 线程中保留一份副本,这样如果 main 线程修改了该变量,t1 线程未必知道
- 使用 volatile 关键字,会让所有线程都会读到变量的修改值
- 在下面的代码中,running 是存在于堆内存的 t 对象中
- 当线程 t1 开始运行的时候,会把 running 值从内存中读到 t1 线程的工作区,在运行过程中直接使用这个副本,
- 并不会每次都去读取堆内存,这样,当主线程修改 running 的值之后,t1 线程感知不到,所以不会停止运行
- 但是这可能是个错误。关于这个例子,在后面会专门花时间再讲
public class Demo {
boolean running = true;
public void test(){
log.debug("test start...");
while (running){
// 1. 经过测试发现,当 while 语句的里面什么代码都没有时,程序不会停止,也就是 t1 无法感知到 主线程已经修改了 running 的值: demo.running = false;
// 2. 但当 while 语句里面有代码时,比如 System.out.println("aaaa"); 这时候,t1 可以感知到 主线程已经修改了 running 的值: demo.running = false; 进而程序会退出循环,结束执行。
// 3. 如果要解决 1. 中的问题,除了上面的第 2. 条,也可以加上 volatile 来修饰变量:volatile boolean running = true;
// 4. 由 1. 和 2. 可以得出,“volatile 关键字,使一个变量在多个线程间可见” 的结论未必正确,因为 2. 中没有添加 volatile 关键词,也解决了 1. 中的问题
}
log.debug("test end...");
}
public static void main(String[] args) {
Demo demo = new Demo();
new Thread(demo :: test,"t1").start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 这里更改 running 的值,目的是使 while() 语句条件得到终止
demo.running = false;
}
}
volatile 不能替代 synchronized(volatile 保证不了原子性)
synchronized 既可以保证原子性又保证了可见性
public class Demo {
volatile int count = 0;
public void test(){
for (int i = 0; i < 10000; i++) {
count ++;
}
}
public static void main(String[] args) {
Demo demo = new Demo();
List<Thread> threads = new ArrayList();
//new 10個线程
for (int i = 0; i < 10; i++) {
threads.add(new Thread(demo::test, "t-" + i));
}
//遍历这个10个线程 依次启动
threads.forEach((o)->o.start());
//等待10个线程执行完
threads.forEach((o)->{
try {
o.join();
} catch (Exception e) {
e.printStackTrace();
}
});
log.debug(demo.count+"");
}
}
多个 AtomicInteger 类连续调用能否构成原子性?
答案是:不能构成原子性,即连续调用,程序会出问题。
因此,在写代码时,要避免连续调用 AtomicInteger 类的方法。但是,只调用一次,是可以的。
public class Demo {
AtomicInteger count = new AtomicInteger(0);
public void test(){
for (int i = 0; i < 10000; i++) {
// 在这里示例中,count.get() 和 count.incrementAndGet() 就不构成原子性,这两个是分别的两个 CAS 了,单个使用没问题,同时使用就出错了。
if(count.get() < 1000){
//count++
count.incrementAndGet();
}
}
}
public static void main(String[] args) {
Demo demo = new Demo();
List<Thread> threads = new ArrayList();
for (int i = 0; i < 10; i++) {
threads.add(new Thread(demo::test, "thread-" + i));
}
threads.forEach((o)->o.start());
threads.forEach((o)->{
try {
o.join();
} catch (Exception e) {
e.printStackTrace();
}
});
log.debug(demo.count+"");
}
}
一道面试题:
实现一个容器,提供两个方法,add,size
写两个线程,线程 1 添加 10 个元素到容器中,线程 2 实现监控元素的个数,
当个数到 5 个时,线程 2 给出提示并结束
CountDownLatch
使用 await 和 countdown 方法替代 wait 和 notify
CountDownLatch 不涉及锁定,当 count 的值为零时当前线程继续运行
相当于是发令枪,运动员线程调用 await 等待,计数到 0 开始运行
当不涉及同步,只是涉及线程通信的时候,用 synchronized 加 wait,notify 就显得太重了
public class Container5 {
volatile List lists = new ArrayList();
public void add(Object o){
lists.add(o);
}
public int size(){
return lists.size();
}
public static void main(String[] args) {
Container5 c = new Container5();
CountDownLatch latch = new CountDownLatch(1);
new Thread(()->{
log.debug("t2启动");
if (c.size() != 5) {
try {
//阻塞
latch.await();//准备
} catch (Exception e) {
e.printStackTrace();
}
log.debug("t2结束");
}
}," t2").start();
new Thread(()->{
log.debug("t1启动");
for (int i = 0; i < 10; i++) {
c.add(new Object());
log.debug("add " + i);
if (c.size() == 5) {
latch.countDown();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
}
}, "t1").start();
}
}
死锁
同一个线程,一次性需要获得两把锁。另一个线程也需要获得两把锁,但获取的顺序和第一个线程相反,这样就形成了死锁,两个线程互相拿不到第二把锁,一直卡在这里等待。
public class LockTest {
//定义两把锁
static Object x = new Object();
static Object y = new Object();
public static void main(String[] args) {
//线程1启动
new Thread(()->{
//获取x的锁
synchronized (x){
log.debug("locked x");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (y){
log.debug("locked x");
log.debug("t1---------");
}
}
},"t1").start();
new Thread(()->{
synchronized (y){
log.debug("locked y");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (x){
log.debug("locked x");
log.debug("t2---------");
}
}
},"t2").start();
}
}