什么是线程安全
所谓线程安全是指我们要确保在多条线程访问的时候,程序能够按照我们预期的行为去执行。
我们通过一个案例去模拟一下线程安全的问题
假设开设多个窗口卖票,窗口我们用线程来模拟。
public class Demo_2 {
public static void main(String[] args) {
//创建票对象
Ticket ticket = new Ticket();
//创建3个窗口
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Ticket implements Runnable {
//共20票
int ticket = 20;
@Override
public void run() {
//模拟卖票
while(true){
if (ticket > 0) {
//模拟选坐的操作
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
}
}
}
}
执行结果
我们会发现,上面的票出现了重复
其实线程安全出现是因为多线程在读写同一个临界资源时发生的。临界资源包括:
多线程共享实例变量
多线程共享静态的公共变量
若有多个线程执行写操作时,我们就需要考虑到线程同步问题,否则就可能会发生线程安全问题。
线程同步(线程安全处理Synchronized)
Java中提供了线程同步机制,能够解决线程安全问题。
线程同步的方法有两种:
同步代码块
同步方法
同步代码块:
synchronized(锁对象){
可能产生线程安全问题的代码
}
同步代码块中的锁对象可以时任意的,但是对于多个线程必须要保证使用同一个锁对象才能保证线程安全。
我们将售票案例中的类改为如下:
class Ticket implements Runnable {
//共20票
int ticket = 20;
//定义锁对象
Object lock = new Object();
@Override
public void run() {
//模拟卖票
while(true){
//同步代码块
synchronized (lock){
if (ticket > 0) {
//模拟选坐的操作
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
}
}
}
}
}
使用了同步代码块,上述的线程安全问题就得到了解决。
同步方法
在声明方法时,加上synchronized。
public void synchronized method(){
可能会产生线程安全问题的代码
}
使用同步方法的方式,对Ticket方法进行修改
class Ticket implements Runnable {
//共20票
int ticket = 20;
//Object lock = new Object();
@Override
public synchronized void run() {
//模拟卖票
while(true){
//synchronized (lock){
if (ticket > 0) {
//模拟选坐的操作
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
}
}
}
}
//}
静态同步加锁:
public static synchronized void method(){
可能会产生线程安全问题的代码
}
静态方法锁的对象是类对象,每个类都有唯一的类对象,获取类对象的方法:类名.class
静态方法和非静态方法同时声明了synchronized,他们之间是非互斥关系,
原因在于:静态方法锁的是类对象,非静态方法锁的是当前方法所属的对象。
死锁
使用同步锁有一个弊端:当线程中出现了多个同步时(多个锁),如果同步中套用了其他同步时。这时容易引发一种现象。程序出现了无限等待。这种现象我们称为死锁。我们要尽量避免这种情况的出现
synchronzied(A锁){
synchronized(B锁){
}
}
我们对死锁进行如下代码演示:
public class Demo_4 {
//测试类
public static void main(String[] args) {
//创建线程任务类对象
ThreadTask task = new ThreadTask();
//创建两个线程
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
//启动线程
t1.start();
t2.start();
}
}
//锁对象
class MyLock {
public static final Object lockA = new Object();
public static final Object lockB = new Object();
}
//线程任务类
class ThreadTask implements Runnable{
int x = new Random().nextInt(1);//0,1
//指定线程要执行的任务代码
@Override
public void run() {
while(true){
if (x%2 ==0) {
//情况一
synchronized (MyLock.lockA) {
System.out.println("LockA");
synchronized (MyLock.lockB) {
System.out.println("LockB");
System.out.println("天上小鸟在飞!");
}
}
} else {
//情况二
synchronized (MyLock.lockB) {
System.out.println("LockB");
synchronized (MyLock.lockA) {
System.out.println("LockA");
System.out.println("地上有只猪在跑");
}
}
}
x++;
}
}
}
本应该无限循环,却因为产生了死锁而不能执行。
Lock接口
经过查阅API,我们会发现,Lock实现提供了比使用synchronized方法和语法可获得更广泛的锁定操作。
Lock接口中常用的方法:
lock() 获取锁
unlock 释放锁
Lock提供了一个更加面向对象的锁,在该锁中提供了更多的操作锁的功能。
我们使用Lock接口中的lock()和unlock()方法代替同步,将案例中的Ticket类进行修改。
class Ticket2 implements Runnable {
//共20票
int ticket = 20;
//创建Lock锁对象
Lock ck = new ReentrantLock();
@Override
public void run() {
//模拟卖票
while(true){
ck.lock();
if (ticket > 0) {
//模拟选坐的操作
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
}
ck.unlock();
}
}
}