线程的同步机制
主要是同java中的synchronized关键字[类锁方式]的实现
关于synchronized关键字的讲解可以参考 synchronized 关键字解析
这里主要是通过售票案例来讲解下线程的同步机制:
使用继承Thread类的方式:
package org.xyz.java.thread.demo03;
/**
* 使用继承Thread的方式来实现多个窗口同时售票的案例场景
* @author kevin.chen
*
*/
public class WindowThread {
/**
* 该案例说明:
* > 1、该案例创建了3个子线程来同时售票,但是3个线程中各种都有一个ticket的成员变量,所以三个线程各种售各种的票,没有达到统一售票的目的(参考WindowT1的代码)
* > 2、要想达到统一售票的目的
* 必须让3个线程共享票数,所以ticket必须由成员变量需改为类变量(参考WindowT2的代码)
* > 3、统一售票的目的达到了,但是还存如下问题:
* 1、不同窗口卖出同号的票
* 2、有的窗口偶尔卖出不存的票号,如0号票或负数票
* > 解决方式:
* a、使用同步代码块(参考WindowT3的代码)
* b、使用同步方法(参考WindowT4的代码)
*/
public static void main(String[] args) {
/*
* 第一种情况
* 结果
* 每个窗口都是卖各自的票,各部相关
*/
// WindowT1 w1 = new WindowT1();
// WindowT1 w2 = new WindowT1();
// WindowT1 w3 = new WindowT1();
/*
* 第二种情况 :存在重票、错票
* 结果
3号窗口: 19
2号窗口: 20
1号窗口: 20
1号窗口: 18
2号窗口: 17
3号窗口: 16
3号窗口: 15
1号窗口: 15
2号窗口: 15
3号窗口: 13
2号窗口: 14
1号窗口: 12
2号窗口: 11
3号窗口: 10
1号窗口: 11
3号窗口: 9
1号窗口: 7
2号窗口: 8
2号窗口: 5
3号窗口: 4
1号窗口: 6
3号窗口: 3
2号窗口: 2
1号窗口: 3
3号窗口: 1
2号窗口: 0
1号窗口: -1
*/
// WindowT2 w1 = new WindowT2();
// WindowT2 w2 = new WindowT2();
// WindowT2 w3 = new WindowT2();
/*
* 第三、四种情况
* 结果
* 预期的结果,正常售票
*/
// WindowT3 w1 = new WindowT3();
// WindowT3 w2 = new WindowT3();
// WindowT3 w3 = new WindowT3();
WindowT4 w1 = new WindowT4();
WindowT4 w2 = new WindowT4();
WindowT4 w3 = new WindowT4();
w1.setName("1号窗口");
w2.setName("2号窗口");
w3.setName("3号窗口");
w1.start();
w2.start();
w3.start();
}
}
// 使用成员变量,票数无法共享
class WindowT1 extends Thread {
// 成员变量 总票数
private int ticket = 20;
@Override
public void run() {
while(true) {
if(ticket > 0) {
System.out.println(Thread.currentThread().getName() + ": " + ticket--);
}else {
break;
}
}
}
}
// 使用类变量,票数可以共享,但存在线程是不安全的
class WindowT2 extends Thread {
// 类变量 总票数
private static int ticket = 20;
@Override
public void run() {
while(true) {
if(ticket > 0) {
try {
// 使用睡眠的方式来让问题放大
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + ticket--);
}else {
break;
}
}
}
}
/*
* 使用类变量,票数可以共享,保障线程的安全性
* 方式一: 使用同步代码块
* 注意: 使用同步代码块,线程使用继承的方式要注意,同步锁一定不能用this来充当对象锁,也不能用Object obj = new Object()的obj来充当,必须使用static关键字修饰为类变量才可,该方式必须使用非本对象锁(或类对象锁)
*/
class WindowT3 extends Thread {
// 类变量 总票数
private static int ticket = 20;
@Override
public void run() {
while(true) {
// 使用this来做同步锁是达不到目的的,因为使用继承的方式,每个this都是不同的对象,所以每个线程竞争的都是各自的同步锁,而非同一把同步锁,所以使用this当同步锁是无法保障线程的安全性
// synchronized(this) {
// 使用类的对象可以保障线程的安全性 Class clazz = WindowT3.class; 这个类对象每个类只要一个,使用类对象等同于将方法同步
synchronized(WindowT3.class) {
if(ticket > 0) {
try {
// 使用睡眠的方式来让问题放大
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + ticket--);
}else {
break;
}
}
}
}
}
/*
* 使用类变量,票数可以共享,保障线程的安全性
* 方式一: 使用同步方法
* 注意: 使用同步方法,同步方法必须使用static修饰
*/
class WindowT4 extends Thread {
// 类变量 总票数
private static int ticket = 20;
private static boolean f = true;
@Override
public void run() {
while(f) {
show();
}
}
// 使用继承的方式 同步方法不使用static修饰,相当于同步代码块中的this本对象锁,依旧无法保障线程的安全性
// synchronized public void show() {
// 使用继承的方式 同步方法必须使用static修饰,类似于同步代码块中的类对象锁
synchronized public static void show() {
if(ticket > 0) {
try {
// 使用睡眠的方式来让问题放大
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + ticket--);
}else {
System.out.println("票已售罄!");
f = false;
}
}
}
使用实现Runnable接口的方式:
package org.xyz.java.thread.demo03;
/**
* 使用实现Runnable接口的方式来实现多个窗口同时售票的案例场景
* @author kevin.chen
*
*/
public class WindowRunnable {
/**
* 该案例说明:
* > 1、该案例创建了3个子线程来同时售票,售票的线程使用实现Runnable的方式,就具备了资源共享的作用,比使用继承的方式有天然的优势(参考WindowR1的代码),但存在线程安全隐患
* > 2、统一售票的目的达到了,但是还存如下问题:
* 1、不同窗口卖出同号的票
* 2、有的窗口偶尔卖出不存的票号,如0号票或负数票
* > 解决方式:
* a、使用同步代码块(参考WindowR2的代码)
* b、使用同步方法(参考WindowR3的代码)
*/
public static void main(String[] args) {
/*
* 第一种情况
* 结果
1号窗口: 20
2号窗口: 19
3号窗口: 18
2号窗口: 17
1号窗口: 17
3号窗口: 16
2号窗口: 15
1号窗口: 14
3号窗口: 13
1号窗口: 12
2号窗口: 11
3号窗口: 10
2号窗口: 9
1号窗口: 8
3号窗口: 7
2号窗口: 6
3号窗口: 5
1号窗口: 4
2号窗口: 3
1号窗口: 2
3号窗口: 1
2号窗口: 0
1号窗口: -1
*/
// WindowR1 w = new WindowR1();
/*
* 第二种情况
* 结果
* 预期的结果,正常售票
*/
// WindowR2 w = new WindowR2();
/*
* 第三种情况
* 结果
* 预期的结果,正常售票
*/
WindowR3 w = new WindowR3();
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();
}
}
//使用成员变量,票数就能直接共享,因为多个线程使用的是同一个对象,不存在多个对象的问题,但存在线程安全问题
class WindowR1 implements Runnable {
// 成员变量 总票数
private int ticket = 20;
@Override
public void run() {
while(true) {
if(ticket > 0) {
try {
// 使用睡眠的方式来让问题放大
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + ticket--);
}else {
break;
}
}
}
}
/*
* 使用成员变量,票数可以共享,保障线程的安全性
* 方式一: 使用同步代码块
* 注意: 使用同步代码块,对比与使用继承的方式即可以使用this本对象锁,也可以使用WindowR2.class类对象锁
*/
class WindowR2 implements Runnable {
// 成员变量 总票数
private int ticket = 20;
@Override
public void run() {
while(true) {
// 此时的this对多个线程来讲是同一个对象,竞争的是同一把对象锁,可以保证线程的安全性,也可以使用类对象锁
synchronized(this) {
if(ticket > 0) {
try {
// 使用睡眠的方式来让问题放大
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + ticket--);
}else {
break;
}
}
}
}
}
/*
* 使用成员变量,票数可以共享,保障线程的安全性
* 方式一: 使用同步方法
* 注意: 使用同步方法,同步方法对static修饰没有要求,可有可无
*/
class WindowR3 implements Runnable {
// 成员变量 总票数
private int ticket = 20;
private boolean f = true;
@Override
public void run() {
while(f) {
show();
}
}
synchronized public void show() {
if(ticket > 0) {
try {
// 使用睡眠的方式来让问题放大
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + ticket--);
}else {
System.out.println("票已售罄!");
f = false;
}
}
}
两者方式的对比:
1、使用继承的时候在资源共享方面不如使用实现的方式,继承想要实现资源共享,必须使用static来将成员变量修饰为类变量
2、在使用同步的来保证线程的安全时,需要时刻注意线程在竞争同步锁的时候,多个线程竞争的锁是否为同一把锁,如果不是,无法保障线程的安全性
3、this关键字在不同的线程实现方式下,需要慎用,因此在使用多线的时候强烈推荐使用实现Runnable接口的方式,而不要继承Thread类的方式。