多线程-线程安全问题
线程安全问题
在多个线程同时访问一个相同的资源的时候会发生线程安全问题。
举个栗子:
买票问题,三个窗口进行买票。
public class ThreadSafe {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket);
Thread t2 = new Thread(ticket);
Thread t3 = new Thread(ticket);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
class Ticket implements Runnable{
private int ticket = 10;//有10张票
@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;
}
}
}
}
运行结果:
很明显可以看出,在三个线程同时去访问Ticket类的时候,票的数量出现的重复和错误(结果为0)的情况。
为什么会出现这种情况呢?
因为线程是并发的,并发就是三个线程同时进行。比如窗口一进入run方法,然后窗口二也进入了run方法,然后两个同时操作ticket的数量,所以数量出现重复。
如何解决呢?
解决线程安全有3中方法:使用同步代码块、同步方法、Lock锁。
1、同步代码块
同步代码块就是将操作共享资源的代码放入由synchronized修饰的代码块中。
在使用同步代码块的时候需要使用一个锁将代码块锁起来,只允许一个线程进行访问。线程在进行操作数据前获得锁,操作结束后将锁释放。
任何对象都可以是锁对象,但是锁对象必须是唯一的
在这里我使用了当前这个对象来作为锁对象,因为我只声明了一个ticket对象,该对象是唯一的。
也可以在Ticket类中声明一个对象,作为锁对象。
public class ThreadSafe {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket);
Thread t2 = new Thread(ticket);
Thread t3 = new Thread(ticket);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
class Ticket implements Runnable{
private int ticket = 10;//有10张票
//Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (this) {//或者使用synchronized(obj)
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖:" + ticket-- + "号票");
} else {
break;
}
}
}
}
}
运行结果:
有结果可以看到,我们加上同步代码块之后,显示的结果就是正确的。
2、同步方法
在方法上加上synchronized关键字即可。同步方法默认的锁对象是当前对象即this对象。
class Ticket implements Runnable{
private int ticket = 10;//有10张票
@Override
public void run() {
while (true) {
sell();
}
}
private synchronized void sell(){
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖:" + ticket-- + "号票");
}
}
}
运行结果:
注意:在使用继承Thread类来创建线程的时候同步方法和同步代码块也会出现安全问题。因为默认使用this为锁对象,在运行的时候创建了三个ticket对象,所以三个线程使用的锁对象不一样。
这里使用同步代码块和同步方法的结果都是一样的,这里就不展示同步方法的代码了。
public class ThreadSafe {
public static void main(String[] args) {
Ticket t1 = new Ticket();
Ticket t2= new Ticket();
Ticket t3 = new Ticket();
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
class Ticket extends Thread{
private int ticket = 10;//有10张票
@Override
public void run() {
while (true) {
synchronized (this) {
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖:" + ticket-- + "号票");
} else {
break;
}
}
}
}
}
运行结果:
从结果可以看出来,每个窗口都卖了10张票,因为三个线程有着不同的锁。
解决办法就是使用当前类作为锁对象。
synchronized(Ticket.class){
//执行语句...
}
为什么使用Ticket.class可以呢?
因为Ticket.class返回的是一个Class类的对象。该对象时唯一的。
3、使用Lock锁
使用Lock锁来解决线程安全问题时,需要使用到Lock对象中的两个方法:
lock() 获得锁
unlock() 释放锁
class Ticket extends Thread{
private int ticket = 10;//有10张票
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock();
try {//使用try-finally可以保证所被释放
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖:" + ticket-- + "号票");
} else {
break;
}
}finally {
lock.unlock();
}
}
}
}
运行结果:
Lock和synchronized的区别?
synchronized会自动释放锁,Lock需要手动启动锁和释放锁。
三种方式优先级
Lock锁 > 同步代码块 > 同步方法