问题引入
多个线程执行的不确定性引起执行结果的不稳定
多个线程对数据的共享,会造成操作的不完整性,会破坏数据。
举例:
创建三个窗口卖票,总票数为100张.使用实现Runnable接口的方式
- 问题:卖票过程中,出现了重票、错票 —>出现了线程的安全问题
- 问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
- 如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来(即上锁)。直到线程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。
Java解决方案:同步机制
在Java中,我们通过同步机制,来解决线程的安全问题。
方式一:同步代码块
synchronized (对象){
// 需要被同步的代码;
}
说明:
- 操作共享数据的代码,即为需要被同步的代码。
- 共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
- 同步监视器,俗称:锁。任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。
补充:
在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类(如:类名.class)充当同步监视器。
继承thread方式:
class Window2 extends Thread{
private static int ticket = 100;
private static Object obj = new Object();
@Override
public void run() {
while(true){
//正确的
// synchronized (obj){
synchronized (Window2.class){//Class clazz = Window2.class,Window2.class只会加载一次
//错误的方式:this代表着t1,t2,t3三个对象
// synchronized (this){
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":卖票,票号为:" + ticket);
ticket--;
}else{
break;
}
}
}
}
}
方式二:同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
public synchronized void show (String name){
// 需要被同步的代码;
}
关于同步方法的总结:
- 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
- 非静态的同步方法,同步监视器是:this
- 静态的同步方法,同步监视器是:当前类本身(类名.class)
同步机制中的锁
- 同步锁机制:
对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。 防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。
- 注意:
必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)
方式三:Lock锁
- 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
class A{
//1.实例化ReentrantLock
private final ReentrantLock lock = new ReentrantLock();
public void m(){
//2.调用锁定方法lock()
lock.lock();
try{
//保证线程安全的代码;
}
finally{
//3.调用解锁方法:unlock()
lock.unlock();
}
}
}
注:如果同步代码有异常,要将unlock()写入finally语句块
synchronized 与 Lock 的对比 :
Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序: Lock —->同步代码块(已经进入了方法体,分配了相应资源)——->同步方法 (在方法体之外)