线程安全问题
考虑如下情景:
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票。
public class TicketSeller extends Thread{
// 定义票的数量
static int ticket = 0; // 取值范围: 0~99
@Override
public void run() {
while (true){
if (ticket < 100){
try {
// 创建延时效果
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(getName() + ":正在卖第" + ticket + "张票!");
}else {
break;
}
}
}
}
public class TicketWindows {
public static void main(String[] args) {
// 创建线程对象
TicketSeller ts1 = new TicketSeller();
TicketSeller ts2 = new TicketSeller();
TicketSeller ts3 = new TicketSeller();
// 命名线程
ts1.setName("窗口1");
ts2.setName("窗口2");
ts3.setName("窗口3");
// 开启线程
ts1.start();
ts2.start();
ts3.start();
}
}
一次运行结果如下:
窗口1:正在卖第2张票!
窗口2:正在卖第2张票!
窗口3:正在卖第2张票!
窗口1:正在卖第3张票!
窗口2:正在卖第3张票!
窗口3:正在卖第3张票!
。。。。。。
窗口2:正在卖第96张票!
窗口1:正在卖第97张票!
窗口3:正在卖第98张票!
窗口2:正在卖第99张票!
窗口1:正在卖第100张票!
窗口3:正在卖第101张票!
窗口2:正在卖第102张票!
我们发现,这个程序并不能达到预期的效果,
①相同的票出现了多次
②出现了超出范围的票
原因是线程执行有随机性。
同步代码块
解决的方式之一是使用同步代码块,即把操作共享数据的代码锁起来。格式如下:
synchronized (锁){
操作共享数据的代码
}
特点1:锁默认打开,有一个线程进去了,锁自动关闭。
特点2:里面的代码全部执行完毕,线程出来,锁自动打开。
利用同步代码块改写后:
public class MyThreadSync extends Thread{
// 表示这个类所有的对象都共享ticketCount数据
static int ticketCount = 0; // 0-99
// 锁对象,可以是任意的对象,但是必须要是唯一的
static Object obj = new Object();
@Override
public void run() {
while (true){
// 同步代码块
synchronized (obj){
if (ticketCount < 100){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticketCount++;
System.out.println(getName() + "正在卖第" + ticketCount + "张票!");
}else {
break;
}
}
}
}
}
这里要注意2点,第一是synchronized代码块要写在while(true)循环的里面,否则这100张票就只能由一个线程卖完为止。第二个注意点是,锁对象要是唯一,这里的对象是obj,需要使用static关键字修饰,在实际开发中一般写当前类的class对象名,如本例中可以写成MyThreadSync.class。
同步方法
同步方法,就是把synchronized关键字加到方法上。格式:
修饰符 synchronized 返回值类型 方法名(参数列表...){方法体...}
- 特点1:同步方法是锁住方法里面所有的代码
- 特点2:锁对象不能自己指定。如果当前方法是非静态的,那么锁对象就是this;如果是静态方法,那么锁对象就是当前类的字节码文件对象。
public class MyThreadSyncFunc implements Runnable {
// 表示电影票的数量
// 注意这里与前面的继承Thread类的方式不一样
// 这里的ticketCount不需要被static修饰
// 因为实现Runnable接口的对象只会创建一个,
// 然后作为参数传给Thread对象
int ticketCount = 0;
@Override
public void run() {
// 4.判断共享数据是否到了末尾,如果没有到末尾
//1.循环
while (true) {
//2.同步代码块(同步方法)
if (method())
break;
}
}
private synchronized boolean method(){
//3.判断共享数据是否到达末尾,则退出
if (ticketCount == 100) {
return true;
} else {
try {
// 睡眠10毫秒
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticketCount++;
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticketCount + "张票!");
}
return false;
}
}
使用代码:
public class MyThreadSyncFuncTest {
public static void main(String[] args) {
MyThreadSyncFunc mtsf = new MyThreadSyncFunc();
// 创建线程对象
Thread t1 = new Thread(mtsf);
Thread t2 = new Thread(mtsf);
Thread t3 = new Thread(mtsf);
// 线程命名
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
// 启动线程
t1.start();
t2.start();
t3.start();
}
}
补充知识点:StringBuilder与StringBuffer是两个相似的类,前者不是线程安全的,而StringBuffer是线程安全的。
Lock锁
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作Lock中提供了获得锁和释放锁的方法:
- void lock():获得锁
- void unlock():释放锁
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化ReentrantLock的构造方法
ReentrantLock():创建一个ReentrantLock的实例
手动上锁/解锁的线程实现类:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyThreadLock extends Thread {
// 表示电影票的数量
static int ticketCount = 0;
// 锁,必须共享一个锁,所以要用static修饰
static Lock lock = new ReentrantLock();
@Override
public void run() {
// 1.循环
while (true) {
// 上锁
lock.lock();
try {
if (ticketCount == 100) {
break;
} else {
Thread.sleep(10);
ticketCount++;
System.out.println(Thread.currentThread().getName() + "在卖第" + ticketCount + "张票!");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 解锁
lock.unlock();
}
}
}
}
使用该线程:
public class MyThreadLockTest {
public static void main(String[] args) {
// 创建线程对象
MyThreadLock mtl1 = new MyThreadLock();
MyThreadLock mtl2 = new MyThreadLock();
MyThreadLock mtl3 = new MyThreadLock();
// 线程命名
mtl1.setName("窗口1");
mtl2.setName("窗口2");
mtl3.setName("窗口3");
// 线程启动
mtl1.start();
mtl2.start();
mtl3.start();
}
}
解锁的代码放在finally语句块中,表示无论如何都要执行解锁操作。
死锁
死锁是一种编程错误,要避免程序陷入死锁的局面从而让程序卡死。在编程实践中要避免锁嵌套。
多线程编程的一般套路
- 循环
- 同步代码块
- 判断共享数据是否到了末尾(到了末尾)
- 判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)