1、synchronized&监视器锁
1.1 synchronized 介绍
在 Java 中,synchronized 是一种关键字,用于实现线程的同步和互斥控制。它可以修饰方法或代码块,用于保护共享资源的访问,避免多个线程同时修改数据而引发的并发问题。
具体来说,synchronized 关键字的作用如下:
-
互斥性:synchronized 关键字确保在同一时刻只有一个线程可以执行被 synchronized 修饰的方法或代码块。当一个线程进入 synchronized 代码块时,它会尝试获取对象的监视器锁(内部锁)。如果该锁已被其他线程占用,则当前线程将被阻塞,直到获取到锁才能执行,从而保证了多个线程对共享资源的互斥访问。
-
可见性:synchronized 关键字不仅提供了互斥性,还保证了线程在释放锁之前对共享变量所做的修改对其他线程是可见的。这意味着当一个线程修改了由 synchronized 保护的共享变量后,其他线程在获取锁之后能够看到最新的值,避免了数据不一致的问题。
使用 synchronized 关键字可以有效地防止多个线程同时访问共享资源而导致的数据竞争、竞态条件等并发问题。它是 Java 中最常用的同步机制之一。
在使用 synchronized 时需要注意以下几点:
-
能够保护共享资源的代码块应尽量缩小范围,避免不必要的锁竞争。
-
synchronized 关键字可以用于实例方法、静态方法和代码块。对于实例方法和代码块,锁对象为当前实例;对于静态方法和代码块,锁对象为当前类的 Class 对象。
-
如果多个线程访问的是不同的对象实例,那么它们之间的锁是不互斥的。也就是说,synchronized 隔离了不同对象实例之间的并发访问。
-
在某些情况下,使用 Lock 接口及其实现类(如 ReentrantLock)可以提供更灵活和精细的锁控制,但需要手动释放锁。
总之,synchronized 关键字是 Java 中用于实现线程同步和互斥控制的重要工具,通过确保临界区的独占性和可见性,有效地保护了共享资源,提高了多线程程序的安全性和正确性。
1.2 监视器锁(Monitor Lock也称为内部锁或互斥锁)
当使用关键字synchronized
时,会涉及到锁的概念。对于synchronized
代码块或方法,锁是一个用于保护共享资源的机制。
在Java中,每个对象都有一个与之关联的监视器锁(也称为内部锁或互斥锁)。当某个线程希望进入一个被synchronized
修饰的代码块或方法时,它必须先获得该对象的锁,才能执行代码块或方法。
Object object2 = new Object();
synchronized (object2) {
// 同步代码块
// 这里的锁就是对象object2
// 只有获得object2的锁才能执行这段同步代码块
}
在上述代码中,使用了一个名为object2
的对象作为锁对象。只有获得了object2
的锁,即成功获取到object2
对象的监视器锁,才能执行synchronized
代码块内的内容。其他尝试获取object2
锁的线程会被阻塞,直到该锁被释放。
因此,该代码片段中的synchronized (object2)
意味着同一时间只有一个线程能够进入与object2
关联的synchronized
代码块,并且其他线程需要等待锁的释放才能继续执行。
请注意,锁对象可以是任意对象,这里的"object2"只是一个示例变量名。重要的是,在多个线程之间共享同一个锁对象,以实现线程同步和互斥控制。
二、线程安全
2 .1 线程安全演示
多线程共同访问成员变量(共享),并进行修改,那么就会存在线程安全问题。
如下:桌子上有20个豆子,每个线程,在桌子上拿走1个豆子,桌子上显示剩余的豆子数量。
当桌子上剩余的豆子数量为0时,抛出异常。
package day06.threadDemo;
public class Demo6 {
public static void main(String[] args) {
Table table = new Table();
Table.Person p1 = table.new Person();
Table.Person p2 = table.new Person();
p1.start();
p2.start();
}
}
class Table{
int beans = 20;
public int getBeans(){
if(beans ==0){
throw new RuntimeException("豆没了");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return beans--;
}
class Person extends Thread{
@Override
public void run() {
while (true){
int beans = getBeans();
System.out.println(this.getName()+":"+beans);
}
}
}
}
执行结果:
当桌子上剩余豆子不为0时,就去桌子上取豆子。
当桌子上还剩1个豆子时,线程0,和线程1,同时去桌子上取豆子(这里面有几率问题,我们开了两个线程,他们不一定同时去取最后一个豆子。如果不同时取,就不会出现线程安全问题,如果正好两个线程同时去取了最后1个),线程0把最后一个豆子取走了,同时线程1也来取,也取走一个,那桌子上就有-1个了。
2.2 解决线程安全问题
1、能不访问共同变量,就不要访问共同变量,能不修改就不修改,
2、加锁(但是会影响性能)
public synchronized int getBeans()
给getBeans这个方法加锁。
一个线程进入这个方法之后,别的线程就进不来了,该线程执行完这个程序后,释放锁,别的线程就可以使用这个程序(举个恶心的例子,多个人上一个厕所,进去一个人后,其他的人就要排队了)
2.3 方法加锁
在方法中添加关键字synchronized
public synchronized int getBeans(){
if(beans ==0){
throw new RuntimeException("豆没了");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return beans--;
}
等价于
public int getBean(){
synchronized (this){
if(beans ==0){
throw new RuntimeException("豆没了");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return beans--;
}
}
synchronized () 中,要执行资源(具体哪个对象的锁),this就是自身的对象。
给整个方法加锁。
整体代码:
这里是以自身Table对象作为锁的资源。
package com.example.analyzestack.controller;
public class SynchronizedTest {
public static void main(String[] args) {
Table table = new Table();
Table.Person p1= table.new Person();
Table.Person p2 = table.new Person();
p1.start();
p2.start();
}
}
class Table{
int beans =20;
public synchronized int getBeans() {
// synchronized 加锁
if(beans==0){
throw new RuntimeException("豆子没有了");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return beans--;
}
class Person extends Thread{
@Override
public void run() {
while (true){
int beans =getBeans();
System.out.println(this.getName()+":"+beans);
}
}
}
}
2.4 静态方法加锁
静态方法,默认使用的是类的class当锁
// 静态方法,默认使用的是类的class当锁(Table.class)
public static synchronized void test1(){
synchronized (Table.class){
}
}
2.5 无效加锁
我们加锁,是对同一个对象加锁。像这种情况
public synchronized int getBeans(){
Object obj = new Object();
synchronized (obj){
if(beans ==0){
throw new RuntimeException("豆没了");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return beans--;
}
给obj加锁,obj不是我们这个类的对象,每次调用getBeans方法都会创建一个obj。这个锁是不管用的。
2.6 死锁
两个线程,线程1占用锁A,去拿锁B。线程2占用锁B,去拿锁A。相互拿不到资源,造成死锁。
举个例子
package day06.threadDemo;
public class Demo8 {
public static void main(String[] args) {
Boo boo = new Boo();
Thread t1 = new Thread(){
@Override
public void run() {
boo.test1();
}
};
Thread t2 = new Thread(){
@Override
public void run() {
boo.test2();
}
};
t1.start();
t2.start();
}
}
class Boo{
private Object object1 = new Object();
private Object object2 = new Object();
public void test1(){
synchronized (object1){
System.out.println(Thread.currentThread().getName()+"-obj1");
synchronized (object2){
System.out.println(Thread.currentThread().getName()+"-obj2");
}
}
}
public void test2(){
synchronized (object2){
System.out.println(Thread.currentThread().getName()+"-obj2");
synchronized (object1){
System.out.println(Thread.currentThread().getName()+"-obj1");
}
}
}
}
运行这个程序,程序一直执行不完。此时,就已经死锁了。