未来可期一位正行走在编程世界中的小白,希望能遇到更多正在努力中的小伙伴。

线程同步机制

一、背景

我以生活中的例子来打开这个问题,例如:我们做火车买票为例子。
创建个窗口,总票数为 100 张,使用实现 Runable 接口的方式

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Window1 implements Runnable{
//总票数
private int ticket = 100;
@Override
public void run() {
while(true){
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+ticket);
ticket--;
}else{
break;
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
Window1 w1 = new Window1();

Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);

t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");

t1.start();
t2.start();
t3.start();
}
}

运行结果
在这里插入图片描述

  • 在卖票的过程中,通过上面的代码运行的代码,在最后出现了重票、错票,从而出现了线程的安全问题。
  • 出现线程安全的原因,就是当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,一起来操作车票。
  • 出现了问题,总要有解决的办法,在 java 多线程就给我们提供了这个解决办法方法。
  • 我先简单的描述一下解决问题的方法,当一个线程 a 在操作 ticket 的时候,其他线程不能参与进来。直到线程 a 操作完 ticket 时,其他线程才可以开始操作 ticket.这种情况即使线程 a 出现了阻塞,也不能被改变。

二、解决方法

方式一:同步代码块

1
2
3
4
语法:
synchronized(同步监视器){
// 需要不同的代码
}

说明:

  • 操作共享数据的代码,即为需要被同步的代码 –> 不能包含代码多了,也不能包含的代码少了。
  • 共享数据,多个数据共同操作的变量,比如:ticket 就是共享数据。
  • 同步监视器俗称 。任何一个类的对象,都可以充当锁。
  • 要求:多个线程必须要用同一把锁。

① 解决继承 Thread 类线程安全问题

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Window2 extends Thread{
//共享数据
private static int ticket = 100;

@Override
public void run(){
//添加同步代码块
// Class clazz =Window2.class,其中Window2.class只会加载一次

while(true){
synchronized(Window2.class){
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName()+":卖票,票号为:"+ticket);
ticket--;
}else{
break;
}
}
}
}
}
public class Window2Test {
public static void main(String[] args) {
Window2 t1 = new Window2();
Window2 t2 = new Window2();
Window2 t3 = new Window2();

t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");

t1.start();
t2.start();
t3.start();
}
}

运行结果:
在这里插入图片描述

② 解决实现 Runable 接口线程安全

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Window1 implements Runnable{
//总票数
private int ticket = 100;
@Override
public void run() {
while(true){
//设置同步代码块
// 此时的this:唯一的Window1的对象
synchronized(this){
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+ticket);
ticket--;
}else{
break;
}
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
Window1 w1 = new Window1();

Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);

t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");

t1.start();
t2.start();
t3.start();
}
}

运行结果:
在这里插入图片描述
补充:

在实现 Runable 接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
在继承 Thread 类创建多线程的方式中,慎用 this 充当同步监视器,考虑使用当前类充当同步监视器。

方式二:同步方法

如果操作共享数据的代码完整的声明在一个方法中,我们不妨碍将此方法声明同步的

① 使用同步方法解决 Thread 类的线程安全问题

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Window3 extends Thread{
private static int ticket = 100;
@Override
public void run(){
while(true){
show();
}
}
private static synchronized void show(){ //同步监视器:Window3.class
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+ticket);
ticket--;
}
}
}
public class WindowTest3 {
public static void main(String[] args) {
Window3 w1 = new Window3();
Window3 w2 = new Window3();
Window3 w3 = new Window3();

w1.setName("窗口一");
w2.setName("窗口二");
w3.setName("窗口三");

w1.start();
w2.start();
w3.start();
}
}

运行结果:
在这里插入图片描述

② 使用同步方法解决实现 Runable 接口线程安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Window4 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while (true){
show();
}
}
private synchronized void show(){ //同步监视器:this
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+ticket);
ticket--;
}
}
}
public class WindowTest4 {
public static void main(String[] args) {
Window4 w = new Window4();

Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);

t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");

t1.start();
t2.start();
t3.start();
}
}

在这里插入图片描述
关于同步方法的总结:

  • 同步方法仍然涉及到同步监视器只是不需要我们显示的声明。
  • 非静态的同步方法,同步监视器是:this
  • 静态的同步方法,同步监视器是: 当前类本身

方式三:Lock 锁 – JDK5.0 新增

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Window5 implements Runnable{
// 1.实例化ReentrantLock
ReentrantLock lock = new ReentrantLock();
private int ticket = 100;

@Override
public void run() {
while(true){
try {
lock.lock();
// 2. 调用锁定方法lock()

if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":售票,票号为:"+ticket);
ticket--;
}else{
break;
}
} finally {
// 3.调用解锁方法:unlock()
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window5 w = new Window5();

Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");

t1.start();
t2.start();
t3.start();
}
}

运行结果:
在这里插入图片描述

总结

使用的优先顺序

1
Lock --> 同步代码块(已经进入方法体,分配了相对应资源) --> 同步方法(在方法体之外)

利弊

  • 同步的方式,解决了线程的安全问题。–> 好处
  • 操作同步代码时,只能一个线程参与,其他线程等待。就相当于是一个的单线程的过程,效率低。

synchronzied 与 Lock 的异同

  • synchronzied 机制在执行完相对应的同步代码块后,自动的释放同步监视器
  • Lock 需要手动的启动(lock()),同时结束时也需要手动的实现(unlock());

死锁

死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁

说明:

  • 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
  • 我们使用同步时,要避免出现死锁。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class A {
public synchronized void foo(B b) { //同步监视器:A类的对象:a
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了A实例的foo方法"); // ①
// try {
// Thread.sleep(200);
// } catch (InterruptedException ex) {
// ex.printStackTrace();
// }
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用B实例的last方法"); // ③
b.last();
}

public synchronized void last() {//同步监视器:A类的对象:a
System.out.println("进入了A类的last方法内部");
}
}
class B {
public synchronized void bar(A a) {//同步监视器:b
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了B实例的bar方法"); // ②
// try {
// Thread.sleep(200);
// } catch (InterruptedException ex) {
// ex.printStackTrace();
// }
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用A实例的last方法"); // ④
a.last();
}

public synchronized void last() {//同步监视器:b
System.out.println("进入了B类的last方法内部");
}
}
public class DeadLock implements Runnable {
A a = new A();
B b = new B();

public void init() {
Thread.currentThread().setName("主线程");
// 调用a对象的foo方法
a.foo(b);
System.out.println("进入了主线程之后");
}

@Override
public void run() {
Thread.currentThread().setName("副线程");
// 调用b对象的bar方法
b.bar(a);
System.out.println("进入了副线程之后");
}

public static void main(String[] args) {
DeadLock dl = new DeadLock();
new Thread(dl).start();


dl.init();
}
}

运行结果:
在这里插入图片描述

线程通信

一、线程通信涉及到的三个方法

wait():一旦执行此方法,当前线程就会进入阻塞状态,并释放同步监视器。
notify():一旦执行方法,就会唤醒被 wait 的第一个线程,如果有多个线程被 wait,就唤醒优先级最高的那个。
notifyAll():一旦执行此方法,就会唤醒所有被 wait()的线程。

代码示例:

线程通信的例子:使用两个线程打印 1-100。线程 1, 线程 2 交替打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Number implements Runnable{
private int number = 1;
Object obj = new Object();
@Override
public void run() {
while(true){
synchronized(obj){
//唤醒线程
obj.notify();
if(number <= 100){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":" +number);
number++;

try {
// 使得调用如下wait()方法的线程进入阻塞状态
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);

t1.setName("线程1");
t2.setName("线程2");

t1.start();
t2.start();
}
}

运行结果:
在这里插入图片描述

二、说明

wait(),notify().notifyAll()**:三个方法必须用在同步代码块或同步方法中。
**wait(),notify(),notifyAll()
:三个方法的调用者必须是同步代码块同步方法的同步监视器。否则会出现IllegalMonitorStateException异常
wait(),notify(),notifyAll()**:三个方法时定义在java.lang.Object**类中。

三、释放锁的操作:

① 当前线程的同步方法、同步代码执行结束。
② 当前线程在同步代码块、同步方法中遇到 break、return 终止该代码块、该方法的继承执行。
③ 当前线程在同步代码块、同步方法中出现未处理的 Error 或 Exception,导致异常结束。
④ 当前线程在同步代码块、同步方法中执行了线程对象的 wait()方法,当前线程暂停,并释放锁

四、不会释放锁的操作

  1. 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield(); 方法暂停当前线程的执行
  2. 线程执行同步代码块时,其他线程调用了线程的 supend()方法将线程挂起,该线程不会释放锁(同步监视器)

五、小结

sleep()与 wait()的异同

  1. 相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
  2. 不同点:
    <1>两个方法声明的位置不同:Thread类中声明sleep()**,Object类中声明wait()**
    <2> 调用的要求不同:sleep()可以在任何需要的场景下调用。wait()必须使用在同步代码块或同步方法中。

java 多线程基础到这里结束了,在写博客同时一边复习自己所学的知识点,给自己不断积累知识点。