目录
一、为什么要线程同步?
因为当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。举个例子,现实生活中,银行取钱问题、火车票多个窗口售票问题等,通常会涉及并发问题,从而需要用到多线程技术。当进程中有多个并发线程进入一个重要数据的代码块时,在修改数据的过程中,很有可能引发线程安全问题,从而造成数据异常。例如,正常逻辑下,同一个编号的火车票只能售出一次,却由于线程安全问题而被多次售出,从而引起实际业务异常。
二、为什么有线程安全问题?
当多个线程同时共享,同一个全局变量或静态变量,做写的操作时,可能会发生数据冲突问题,也就是线程安全问题。但是做读操作是不会发生数据冲突问题。
案例:需求现在有20张火车票,有三个窗口同时抢火车票,请使用多线程模拟抢票效果。
代码:
class SellTickets01 implements Runnable {
int tickets = 20;
@Override
public void run() {
// 同步代码块
while (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
sale();
}
}
private void sale(){
if(tickets > 0){
System.out.println(Thread.currentThread().getName() + " -->售出第 " + (20-tickets+1) + " 张票");
tickets--;
}else {
System.out.println(Thread.currentThread().getName() + " -->售票结束!");
}
}
}
/**
* 需求现在有30张火车票,有两个窗口同时抢火车票,请使用多线程模拟抢票效果
*/
public class ThreadSync01{
public static void main(String[] args) {
SellTickets01 tickets = new SellTickets01();
Thread t1 = new Thread(tickets, "1号窗口");
Thread t2 = new Thread(tickets, "2号窗口");
Thread t3 = new Thread(tickets, "3号窗口");
t1.start();
t2.start();
t3.start();
}
}
运行结果:
我们会发现3个窗口同时出现重复票数,
结论发现,多个线程共享同一个全局成员变量时,做写的操作可能会发生数据冲突问题。
三、线程安全解决办法
问:如何解决多线程之间线程安全问题?
答:使用多线程之间同步synchronized或使用锁(lock)。
问:为什么使用线程同步或使用锁能解决线程安全问题呢?
答:将可能会发生数据冲突问题(线程不安全问题),只能让当前一个线程进行执行。代码执行完成后释放锁,让后才能让其他线程进行执行。这样的话就可以解决线程不安全问题。
问:什么是多线程之间同步?
答:当多个线程共享同一个资源,不会受到其他线程的干扰。
3.1 同步代码块
什么是同步代码块?
答:就是将可能会发生线程安全问题的代码,给包括起来。
synchronized(同一个数据){
可能会发生线程冲突问题
}
就是同步代码块 。同步代码块会被JVM自动加上内置锁,从而实现同步。
对象如同锁,持有锁的线程可以在同步中执行,没持有锁的线程即使获取CPU的执行权,也进不去
使用synchronized(线程同步)必须有一些条件:
1.必须要有两个或者两个以上的线程需要发生同步。2.多个线程想同步,必须使用同一把锁
3.保证只有一个线程进行执行
synchronized原理:
1.首先有一个线程已经拿到了锁,其他线程已经有cup执行权,一直排队,等待释放锁。
2.锁是在什么时候释放?代码执行完毕或者程序抛出异常都会被释放掉。
3.锁已经被释放掉的话,其他线程开始进行抢锁(资源竞争),谁抢到谁进入同步中去,其他线程继续等待。好处:解决了多线程的安全问题
弊端:多个线程需要判断锁,较为消耗资源、抢锁的资源。
synchronized 只能有一个线程进行执行(就像厕所只有一个坑,好多人等着上厕所,谁进入厕所谁上锁,其他人在外等待),这个线程不释放锁的话,其他线程就一直等,就会产生死锁问题。
代码:
在介绍之前我们先说一下临界区,在Java中代码段访问了同一个对象或数据,那么这个代码段就叫做临界区。上面程序中run()方法中循环里面的代码就叫做临界区。我们需要对这个临界区进行保护,这样就用到了线程的同步。
class SellTickets2 implements Runnable {
int tickets = 20;
@Override
public void run() {
// 同步代码块
while (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
sale();
}
}
//如果将当前方法声明为static,那么能替换this锁的方式就是 SyncThread.class
private void sale(){
//synchronized使用的是this对象锁,如果某个线程获得了锁,那么其他线程只可以执行当前对象的非临界区(非加锁程序块)
//获得锁的线程在进入下一个加锁临界区(加锁块)则不用再次获取锁,这就是锁的可重如性。
//JVM负责跟踪对象被加锁的次数,在任务第一次给对象加锁的时候,计数变成1,每当这个相同的任务在这个对象上获得锁时,计数都会递增(只有首先获得锁的任务才能继续获取锁)
//每当任务离开一个synchronized的方法,计数递减,当计数为0时,锁被完全释放,此时别的任务线程就可以使用此资源。
synchronized (this) {
if(tickets > 0){
System.out.println(Thread.currentThread().getName() + " -->售出第 " + (20-tickets+1) + " 张票");
tickets--;
}else {
System.out.println(Thread.currentThread().getName() + " -->售票结束!");
}
}
}
}
/**
* 需求现在有20张火车票,有三个窗口同时抢火车票,请使用多线程模拟抢票效果
*/
public class ThreadSync02{
public static void main(String[] args) {
SellTickets2 tickets = new SellTickets2();
Thread t1 = new Thread(tickets, "1号窗口");
Thread t2 = new Thread(tickets, "2号窗口");
Thread t3 = new Thread(tickets, "3号窗口");
t1.start();
t2.start();
t3.start();
}
}
运行结果:
上面使用synchronized关键字的时候需要一个对象,这里的对象可以是任意对象,我们在这里选择的是this对象,你也可以选择String、Object类的对象。每一个对象都有一个监视器或者叫锁。当我将obj作为参数传给synchronized关键字的时候,就相当于给这段代码加了一个锁,当我执行到这段代码的时候,先判断是否加锁,如果没有加锁,先将其加锁然后执行代码。如果加锁了只能等待。等到给代码加锁的线程执行完成之后,会将锁打开。
3.2 同步方法 (函数)
即有synchronized关键字修饰的方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
什么是同步函数?
答:在方法上加上synchronized进行修饰 称为同步函数。同步函数使用的是什么锁?答:this锁
代码:
class SellTickets3 implements Runnable {
int tickets = 20;
@Override
public void run() {
// 同步代码块
while (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
sale();
}
}
private synchronized void sale(){
if(tickets > 0){
System.out.println(Thread.currentThread().getName() + " -->售出第 " + (20-tickets+1) + " 张票");
tickets--;
}else {
System.out.println(Thread.currentThread().getName() + " -->售票结束!");
}
}
}
/**
* 需求现在有20张火车票,有三个窗口同时抢火车票,请使用多线程模拟抢票效果
*/
public class ThreadSync03{
public static void main(String[] args) {
SellTickets3 tickets = new SellTickets3();
Thread t1 = new Thread(tickets, "1号窗口");
Thread t2 = new Thread(tickets, "2号窗口");
Thread t3 = new Thread(tickets, "3号窗口");
t1.start();
t2.start();
t3.start();
}
}
运行结果:
3.3 Lock锁机制
通过创建Lock对象,采用lock()加锁,采用unlock()解锁,来保护指定代码块。 在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
ReenreantLock类的常用方法有:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用
代码:
class SellTickets implements Runnable {
int tickets = 20;
private final ReentrantLock lock = new ReentrantLock(true);
@Override
public void run() {
// 同步代码块
while (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
sale();
}
}
private void sale(){
lock.lock();//获取锁
try {
if(tickets > 0){
System.out.println(Thread.currentThread().getName() + " -->售出第 " + (20-tickets+1) + " 张票");
tickets--;
}else {
System.out.println(Thread.currentThread().getName() + " -->售票结束!");
}
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();//释放锁
}
}
}
public class ThreadLock {
public static void main(String[] args) {
SellTickets tickets = new SellTickets();
Thread t1 = new Thread(tickets, "1号窗口");
Thread t2 = new Thread(tickets, "2号窗口");
Thread t3 = new Thread(tickets, "3号窗口");
t1.start();
t2.start();
t3.start();
}
}
运行结果:
四、最后总结
由于synchronized是在JVM层面实现的,因此系统可以监控锁的释放与否;而ReentrantLock是使用代码实现的,系统无法自动释放锁,需要在代码中的finally子句中显式释放锁lock.unlock()。
另外,在并发量比较小的情况下,使用synchronized是个不错的选择;但是在并发量比较高的情况下,其性能下降会很严重,此时ReentrantLock是个不错的方案。
补充:
在使用synchronized 代码块时,可以与wait()、notify()、nitifyAll()一起使用,从而进一步实现线程的通信。其中,wait()方法会释放占有的对象锁,当前线程进入等待池,释放cpu,而其他正在等待的线程即可抢占此锁,获得锁的线程即可运行程序;线程的sleep()方法则表示,当前线程会休眠一段时间,休眠期间,会暂时释放cpu,但并不释放对象锁,也就是说,在休眠期间,其他线程依然无法进入被同步保护的代码内部,当前线程休眠结束时,会重新获得cpu执行权,从而执行被同步保护的代码。
wait()和sleep()最大的不同在于wait()会释放对象锁,而sleep()不会释放对象锁。notify()方法会唤醒因为调用对象的wait()而处于等待状态的线程,从而使得该线程有机会获取对象锁。调用notify()后,当前线程并不会立即释放锁,而是继续执行当前代码,直到synchronized中的代码全部执行完毕,才会释放对象锁。JVM会在等待的线程中调度一个线程去获得对象锁,执行代码。
需要注意的是,wait()和notify()必须在synchronized代码块中调用。
notifyAll()是唤醒所有等待的线程。