一、前言
线程锁是什么?有什么用?怎么用?
先来看一段代码:
public class MyService{
private String username = "AA";
private String password = "aa";
//非线程安全的get方法
public void getValue() {
System.out.println(Thread.currentThread().getName()+" : "+username+" "+password);
}
//线程安全的set方法
//设置username值停顿1s再设置password值
synchronized public void setValue(String username,String password){
this.username = username;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.password = password;
}
public static void main(String[] args) throws InterruptedException {
MyService service = new MyService();
//先启动线程A,200毫秒后再启动线程B
Thread thread1 = new Thread(() -> service.setValue("BB","bb"),"Thread-A");
thread1.start();
Thread.sleep(200);
Thread thread2 = new Thread(service::getValue,"Thread-B");
thread2.start();
}
}
打印结果:
Thread-B : BB aa
出现脏读是因为 getValue 方法不是同步的,所以可以在任意时候进行调用。解决方法就是加上同步 synchronized 关键字,代码如下:
synchronized public void getValue() {
System.out.println(Thread.currentThread().getName()+" : "+username+" "+password);
}
运行结果:
Thread-B : BB bb
下面开始介绍sysnchronized的特性和使用。
二、特性
2.1 synchronized 锁重入
关键字 synchronized 拥有锁重入的功能,也就是在使用 synchronized 时,当一个线程得到一个锁后,再次请求获取此锁时是可以再次得到该锁的。
这也证明了在一个 synchronized 方法 / 块的内部调用本类的其他 synchronized 方法 / 块,若锁一样,是可以得到锁的。
示例代码:
public class MyService{
synchronized public void service1(){
System.out.println("service1");
service2();
}
synchronized public void service2(){
System.out.println("service2");
}
}
或
public class MyService{
synchronized public void service1(){
System.out.println("service1");
Thread.sleep(1000);
Service1();
}
}
“可重入锁”通俗的理解就是:线程自己可以再次获取自己的当前持有的锁。可重入锁也支持在父子类继承的环境中。
示例代码:
public class MyServiceChild extends MyService{
synchronized public void service(){
System.out.println(“service1”);
this.service2();
}
}
2.2 不具有继承性
同步不可以继承。子类继承父类的同步方法时还需要添加 synchronized 关键字才能保持同步。
三、sysnchronized的锁对象
3.1 同步静态方法
示例:
public class ThreadTest {
synchronized public static void service() {
System.out.println(Thread.currentThread().getName() + " begin: " + System.currentTimeMillis());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " end: " + System.currentTimeMillis());
}
synchronized public static void service2(){
System.out.println(Thread.currentThread().getName() + " begin: " + System.currentTimeMillis());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " end: " + System.currentTimeMillis());
}
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
ThreadTest.service();
}
}, "Thread-A").start();
new Thread(new Runnable() {
@Override
public void run() {
new ThreadTest().service2();
}
}, "Thread-B").start();
}
}
结果
Thread-A begin: 1612403849173
Thread-A end: 1612403852173
Thread-B begin: 1612403852173
Thread-B end: 1612403855174
小结
由上面示例可知,线程A执行完了,才执行线程B。原因是两个方法都定义为静态同步方法,因此两个方法的锁对象都为当前类的class对象,所以两个方法是同步的。
3.2 同步普通方法
代码
public class ThreadTest {
synchronized public void service() {
System.out.println(Thread.currentThread().getName() + " begin: " + System.currentTimeMillis());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " end: " + System.currentTimeMillis());
}
synchronized public void service2(){
System.out.println(Thread.currentThread().getName() + " begin: " + System.currentTimeMillis());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " end: " + System.currentTimeMillis());
}
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
new ThreadTest().service();
}
}, "Thread-A").start();
new Thread(new Runnable() {
@Override
public void run() {
new ThreadTest().service2();
}
}, "Thread-B").start();
}
}
结果
Thread-A begin: 1612404203824
Thread-B begin: 1612404203829
Thread-A end: 1612404206825
Thread-B end: 1612404206830
小结
由上面示例可知,两个方法是异步执行的,互不干预执行,原因是普通方法的锁对象是当前类的实例对象,因此两个线程调用的分别是两个不同的实例方法,锁不同,所以是异步的。
当我们将main方法改写如下:
public static void main(String[] args) {
ThreadTest2 threadTest2 = new ThreadTest2();
new Thread(new Runnable() {
@Override
public void run() {
threadTest2.service();
}
}, "Thread-A").start();
new Thread(new Runnable() {
@Override
public void run() {
threadTest2.service2();
}
}, "Thread-B").start();
}
两个方法的执行就因此变成了同步,因为,他们的锁对象都是同一个实例。
3.3 同步代码块
介绍
同步代码块的锁对象可以是任意对象,因此比较特殊。
public void service() {
synchronized(XX){
System.out.println(Thread.currentThread().getName() + " begin: " + System.currentTimeMillis());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " end: " + System.currentTimeMillis());
}
}
public void service2(){
synchronized(XX){
System.out.println(Thread.currentThread().getName() + " begin: " + System.currentTimeMillis());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " end: " + System.currentTimeMillis());
}
}
3.3.1 锁对象为this
这时需要注意线程调用同步方法使用的实例,如下代码:
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
new ThreadTest3().service();
}
}, "Thread-A").start();
new Thread(new Runnable() {
@Override
public void run() {
new ThreadTest3().service2();
}
}, "Thread-B").start();
}
可知,两个方法的锁对象,使用的不是同一个实例,因此是异步执行。
改写:
public static void main(String[] args) {
ThreadTest2 threadTest2 = new ThreadTest3();
new Thread(new Runnable() {
@Override
public void run() {
threadTest2.service();
}
}, "Thread-A").start();
new Thread(new Runnable() {
@Override
public void run() {
threadTest2.service2();
}
}, "Thread-B").start();}
可知,两个方法同步代码块使用的锁对象是同一个实例,因此是同步执行。
3.3.2 锁对象为this.getClass
首先我们需要知道,任意类都对应有且仅有一个Class对象(单例的),不论由哪个实例对象get出来的class,其实都是同一个。
因此,只要该类下,同步代码块锁对象为这个类的class对象,都会时同步执行。
3.3.3 锁对象为变量
前提知识:
Java有三种类型变量:局部、成员、静态
字面量的变量如下:
public String lock = “lock”;
Integer lock = 1;
Character lock = ‘l’;
非字面量如下:
Integer i = new Integer(1);
String lock = new String(“lock”);
简言之,八种包装类型都可以定义字面值(不是new出来的)。
字面值会保存在方法区的常量池中(会被共用),而通过new产生的对象存在堆中。
进入正题:
public class ThreadTest4 {
public String lock = "lock";
public String lock2 = "lock";
public void service() {
synchronized(lock){
System.out.println(Thread.currentThread().getName() + " "+lock.hashCode());
System.out.println(Thread.currentThread().getName() + " begin: " + System.currentTimeMillis());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " end: " + System.currentTimeMillis());
}
}
public void service2(){
synchronized(lock2){
System.out.println(Thread.currentThread().getName() + " "+lock2.hashCode());
System.out.println(Thread.currentThread().getName() + " begin: " + System.currentTimeMillis());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " end: " + System.currentTimeMillis());
}
}
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
new ThreadTest4().service();
}
}, "Thread-A").start();
new Thread(new Runnable() {
@Override
public void run() {
new ThreadTest4().service2();
}
}, "Thread-B").start();
}
}
运行结果,是同步的。
可见,两个方法的对象锁都是等值的String类型数据,根据上述字面值知识,可知两个方法使用的锁对象是同样的。
更改如下:
public String lock = “lock”;
public String lock2 = new String(“lock”);
运行结果,是异步的。
可知两个方法的锁对象不一样。
再将两个方法锁改如下:
public void service(String lock) {
//…
}
public void service2(String lock) {
//…
}
线程调用时,两个线程都直接传入值“1”
结果可知,等值时,同步,不等值时,异步。可见两个局部变量,也是共享常量池中的同一个字面值,因此他们的锁对象是一样的。
再改,两个线程都传入:new String(“1”)
结果可知,异步。因为对象存在堆中,不是线程共享的,因此锁对象不一样。
同理的,其他几种基本数据包装类型,也有次特性。
小结:
成员、局部变量的字面值缓存在常量池中,会被线程共享,因此用于作锁对象时,等值时会被认为同一个锁。
静态变量(不论字面值还是引用类型),会存在方法区中,且仅能有一个,被线程共享。
由于常量池的特性,容易出现问题,因此,一般推荐使用字面值作为锁对象。推荐 new Object() 实例化一个 Object 对象,但它并不放入缓存中。
四、锁的改变
代码:
class ThreadTest5 {
private String lock = "123";
public void service(){
synchronized (lock) {
System.out.println(Thread.currentThread().getName()+" begin: " + System.currentTimeMillis());
lock = "456";
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" end: " + System.currentTimeMillis());
}
}
public static void main(String[] args) throws InterruptedException {
ThreadTest5 test = new ThreadTest5();
new Thread(new Runnable() {
@Override
public void run() {
test.service();
}
},"Thread-A").start();
Thread.sleep(50);
new Thread(new Runnable() {
@Override
public void run() {
test.service();
}
},"Thread-B").start();
}
}
分析:
运行结果:
Thread-A begin: 1537019992452
Thread-B begin: 1537019992652
Thread-A end: 1537019994453
Thread-B end: 1537019994653
为什么是异步?因为 50ms 过后,第二個线程取得的锁是“456”,所以两个线程持有的是不同的,故是异步执行。
把 lock = “456” 放在 Thread.sleep(2000) 之后,再次运行。
Thread-A begin: 1537020101553
Thread-A end: 1537020103554
Thread-B begin: 1537020103554
Thread-B end: 1537020105558
线程 A 和线程 B 持有的锁都是“123”,虽然将锁改成了“456”,但结果还是同步的,因为 B 再锁被更改前启动了,争抢的锁是“123”。
还需要提示一下,对于锁为复杂对象(含属性值),只要对象不变,即使对象的属性被改变,运行的结果还是同步的。
五、sysnchronized造成的多线程死锁
案例:
class ThreadTest6 implements Runnable {
public String username;
public Object locak1 = new Object();
public Object locak2 = new Object();
public void setFlag(String username){
this.username = username;
}
@Override
public void run() {
if (username.equals("a")){
synchronized (locak1){
System.out.println("username:"+username);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locak2){
System.out.println(" 按 lock1-》lock2 执行 ");
}
}
}
if (username.equals("b")){
synchronized (locak2){
System.out.println("username:"+username);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locak1){
System.out.println(" 按 lock2-》lock1 执行 ");
}
}
}
}
public static void main(String[] args) throws InterruptedException {
ThreadTest6 dealThread = new ThreadTest6();
dealThread.setFlag("a");
Thread threadA = new Thread(dealThread);
threadA.start();
Thread.sleep(100);
dealThread.setFlag("b");
Thread threadB = new Thread(dealThread);
threadB.start();
}
}