synchronized处理线程安全问题
关键词:
多线程
线程安全
共享数据
同步监视器
synchronized
static
反射
一、实现多线程的方法
①继承Thread类
②实现Runnable接口
③实现Callable接口
④使用线程池
1.1 经典问题:火车站三个窗口卖100张票?
1.2 实现方法一:继承Thread类(存在线程安全)
/**
* @Author: Ron
* @Create: 2020 19:56
*/
public class SaleTicket {
public static void main(String[] args) {
Ticket thread1 = new Ticket();
Ticket thread2 = new Ticket();
Ticket thread3 = new Ticket();
thread1.setName("窗口001");
thread2.setName("窗口002");
thread3.setName("窗口003");
thread1.start();
thread2.start();
thread3.start();
}
}
class Ticket extends Thread {
private static int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket--);
} else {
break;
}
}
}
}
注意点:
main方法中由于继承关系,新建了三个Ticket对象,由于卖票操作的是相同的100张票,所以在Ticket类中,ticket属性前加关键字static修饰,保证三个线程操作的是同一组数据
1.3 实现方法二:实现Runnable接口(存在线程安全)
/**
* @Author: Ron
* @Create: 2020 20:00
*/
public class SaleTicket002 {
public static void main(String[] args) {
Saler saler = new Saler();
Thread thread1 = new Thread(saler, "售票员001");
Thread thread2 = new Thread(saler, "售票员002");
Thread thread3 = new Thread(saler, "售票员003");
thread1.start();
thread2.start();
thread3.start();
}
}
class Saler implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket--);
} else {
break;
}
}
}
}
注意点:
因为实现Runnable接口,main方法中只是new了一个Saler类,三个线程操作的是同一组数据,所以在Saler类中,ticket前不加关键字static也可以
以上,无论哪种方式实现卖票程序,都会出现线程安全问题,即错票问题(if判断后让线程休眠,会更容易体现出这个问题(线程虚假唤醒))
二、解决线程安全问题的方法
在Java中,我们通过同步机制,来解决线程的安全问题
①同步代码块
②同步方法
③Lock锁(JDK5.0新增)
2.1 方式一:同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
如实现Runnable接口的方法,加上同步代码块:
/**
* @Author: Ron
* @Create: 2020 20:00
*/
public class SaleTicket002 {
public static void main(String[] args) {
Saler saler = new Saler();
Thread thread1 = new Thread(saler, "售票员001");
Thread thread2 = new Thread(saler, "售票员002");
Thread thread3 = new Thread(saler, "售票员003");
thread1.start();
thread2.start();
thread3.start();
}
}
class Saler implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
synchronized (this) {
if (ticket > 0) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖第 " + ticket-- + "票");
} else {
break;
}
}
}
}
}
2.2 关于同步代码块的总结
说明:
- 操作共享数据的代码,即为需要被同步的代码( 不能包含代码多了,也不能包含代码少了)。
- 共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
- 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁( 多个线程必须要共用同一把锁)。
补充:
在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器(如上面实现Runnable接口的代码,main方法中只new了一次Saler类)。在继承Thread类创建多线程的方式中,慎用this充当同步监视器(如上面继承Thread类的代码,main方法中new了三次Ticket类,如果用this的话,就会产生三把锁,不能解决线程安全问题,可以使用反射,Ticket.class作为当前锁),考虑使用当前类充当同步监视器。
2.3 方式二:同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们可以将此方法声明同步的。
public synchronized void 方法() {
//操作的共享数据
}
使用同步方法处理实现Runnable接口的方法:
/**
* @Author: Ron
* @Create: 2020 20:00
*/
public class SaleTicket002 {
public static void main(String[] args) {
Saler saler = new Saler();
Thread thread1 = new Thread(saler, "售票员001");
Thread thread2 = new Thread(saler, "售票员002");
Thread thread3 = new Thread(saler, "售票员003");
thread1.start();
thread2.start();
thread3.start();
}
}
class Saler implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
method();
if (ticket == 0) {
break;
}
}
}
private synchronized void method() {
if (ticket > 0) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖第 " + ticket-- + "票");
}
}
}
2.3 关于同步方法的总结
同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
非静态的同步方法,同步监视器是:this
静态的同步方法,同步监视器是:当前类本身
三、使用的优先顺序
Lock —> 同步代码块(已经进入了方法体,分配了相应资源 ) —>同步方法(在方法体之外)
利:同步的方式,解决了线程的安全问题。
弊:操作同步代码时,只能一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。