线程的同步
什么是线程安全问题
在开发中,在多线程数据共享的时候会遇到线程的安全问题,即多个线程同时进行访问时,一个线程对共享的数据还未操作完成,另一个线程也参与进来。
e.g.
public class SellTicket {
public static void main(String[] args) {
TicketWindow window = new TicketWindow();
Thread threadFirst = new Thread(window);
Thread threadSecond = new Thread(window);
Thread threadThird = new Thread(window);
threadFirst.setName("窗口一");
threadSecond.setName("窗口二");
threadThird.setName("窗口三");
threadFirst.start();
threadSecond.start();
threadThird.start();
}
}
class TicketWindow implements Runnable{
private int ticket = 50;
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ": " + ticket);
ticket--;
} else {
break;
}
}
}
}
结果:
因为CPU资源分配是随机的,所以在一个线程执行的过程中很可能插入其他线程,多个线程可能同时访问同一数据,这就使得线程变得不安全。
如何解决
Java中,通过同步机制,来解决问题
方法一:同步代码块
synchronized(同步监视器){
//需要被同步的代码(操作共享数据的代码)
}
需要同步的代码:操作共享数据的代码,不能包含多也不能包含少。包含太多可能会影响逻辑,也会降低效率
共享数据:多个线程共同操作的数据
同步监视器:任何一个类的对象,同步监视器也称之为锁。要求,几个线程中的“锁”只能有一个,即同步监视器为同一个对象
补充:
当通过实现Runnable接口来创建线程,也可以把this来当作“锁”,只需要保证多线程中“锁”的唯一性就好
然而是通过继承Thread类的方式创建线程时,一般用static对象,或可以把当前类作为同步监视器,例如:TicketWindow.class
加上线程锁的多线程代码
public class SellTicket {
public static void main(String[] args) {
TicketWindow window = new TicketWindow();
Thread threadFirst = new Thread(window);
Thread threadSecond = new Thread(window);
Thread threadThird = new Thread(window);
threadFirst.setName("窗口一");
threadSecond.setName("窗口二");
threadThird.setName("窗口三");
threadFirst.start();
threadSecond.start();
threadThird.start();
}
}
class TicketWindow implements Runnable{
private int ticket = 50;
//private String monitor = "同步监视器";
@Override
public void run() {
while (true){
//加上同步代码块synchronized(同步监视器){}
synchronized(this){//monitor
if (ticket > 0){
System.out.println(Thread.currentThread().getName() + ": " + ticket);
ticket -- ;
}else {
break;
}
}
}
}
}
结果:
给线程加上锁,使得争夺到“锁”对象的线程执行完该线程之前,其他线程处于阻塞状态。
方法二:同步方法
如果多线程中操作共享数据的代码恰好是一个方法,可以使用同步方法的方式解决线程安全问题
public void synchronized 操作共享数据的方法(){
//操作共享数据的代码
}
public class SellTicket {
public static void main(String[] args) {
TicketWindow window = new TicketWindow();
Thread threadFirst = new Thread(window);
Thread threadSecond = new Thread(window);
Thread threadThird = new Thread(window);
threadFirst.setName("窗口一");
threadSecond.setName("窗口二");
threadThird.setName("窗口三");
threadFirst.start();
threadSecond.start();
threadThird.start();
}
}
//同步方法
class TicketWindow implements Runnable{
private int ticket = 50;
@Override
public void run() {
while (true){
show();
}
}
//操作共享数据的方法 在方法名前加上synchronized
private synchronized void show() {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ": " + ticket);
ticket--;
}
}
tips:其实用同步方法解决线程的安全问题也是给线程加了线程锁
在非静态方法上加上synchronized 同步监视器为this(调用该方法的对象) --- 一般用于实现Runnable接口创建的线程
在静态方法上加上synchronized 同步监视器为当前类 --- 一般用于用继承Thread类创建的线程
结果:
方法三:Lock锁
介绍:
Lock锁是一种更强大的线程同步机制,通过显示定义同步锁的方式来实现同步。同步锁使用Lock对象来充当,Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了共享资源的独家访问
Lock锁不同于以上两种方法,以上两种方法时通过synchronized来实现的,而Lock是一个接口,一般使用其实现类ReentrantLock
ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义
使用:
e.g.
public class LockTest {
public static void main(String[] args) {
TicketWindow window = new TicketWindow();
Thread threadFirst = new Thread(window);
Thread threadSecond = new Thread(window);
Thread threadThird = new Thread(window);
threadFirst.setName("窗口一");
threadSecond.setName("窗口二");
threadThird.setName("窗口三");
threadFirst.start();
threadSecond.start();
threadThird.start();
}
}
//Lock接口 ReentrantLock接口实现类
class TicketWindow implements Runnable{
private int ticket = 50;
//实例化ReentrantLock
//构造器中可以填入参数,false代表不公平,true代表公平,不填写默认为false
//公平就是先进先出,多个线程有序的执行lock()锁包含的范围
// ReentrantLock reentrantLock = new ReentrantLock(true);
ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
//把lock放在try中
try {
//上锁
lock.lock();
//操作共享数据的代码
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ": " + ticket);
ticket--;
}else{
break;
}
}finally {
//解锁(解锁方法放在finally中)
lock.unlock();
}
}
}
}
结果:
tips:使用时需要先创建Lock实现类ReentrantLock的对象,然后再try中通过lock( )方法给操作共享数据的代码上锁,之后在finally中通过unlock( )方法解锁。
方式三和方式一,方式二的区别
相同:
两者都可以解决线程安全问题
不同:
Lock的方式需要 手动启动同步(lock( )方法)手动结束同步(unlock( ))方法,而synchronized机制是在执行完操作共享数据的代码后自动释放同步监视器。即Lock是显式锁,synchronized是隐式锁(离开作用域自动释放)。
Lock只有代码块锁,而synchronized有代码块锁和方法锁
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并具有更好的扩展性,提供很多实现类
使用的优先顺序:
Lock > 同步代码块 > 同步方法
同步的好处与影响
好处
线程的安全问题得到解决,多个线程不会同时操作共享数据
影响
一个线程对共享数据操作时其他线程阻塞,相当于一个单线程过程,程序执行变慢了一些,效率变低
学习笔记!