目录
2.线程同步机制
2.1.线程安全问题产生原因
- 线程安全问题产生原因:多线程访问了共享的数据,会产生线程安全问题。
- 电影院卖100张票,若只有一个窗口卖着100张票没问题,单线程程序不会出现线程安全问题。
- 若电影院开放3个窗口一起卖票,但卖的票不同,也不会出现问题,多线程程序没有访问共享数据,无线程安全问题。
- 若电影院开放3个窗口卖的票是一样的,就会出现线程安全问题,多线程访问了共享数据,会产生线程安全问题。
- (使用多线程卖票,一个卖票线程任务可由多个线程(窗口)执行,为了避免线程失去cpu执行权产生线程安全,使用锁解决,只有p1窗口卖完一张票后p2窗口才可以继续卖第二张票,p2等待时间非常短,若是8线程,则可以同时开通8个窗口卖票,但任意一个窗口卖票同时其他窗口只能等待这个线程释放锁和cpu执行权,这期间各个窗口需要等待,时间很短,这是单CPU情况,若是多CPU资源是可以实现真正意义上的并行执行,但对于卖票这种访问共享数据,即使有多CPU,在使用锁机制后还是要排队。单线程卖票只能开一个窗口只有一个通道执行任务,多线程有多个通道执行多个线程任务(高速切换),若执行同一个线程任务且有共享数据就会出现线程安全问题)
2.2.线程安全问题代码实现
/**
* 多线程实现卖票案例: 线程安全问题的代码实现
*/
//1.创建Runnable接口的实现类
public class Demo8RunnableImpl_Ticket implements Runnable{
//定义一个多个线程共享的票源
private int ticket=10;
//2.重写实现类中的run方法,设置线程任务为卖票
@Override
public void run() {
//使用死循环,让卖票操作重复执行
while(true){
//先判断票是否存在
if(ticket>0){
//为提高安全问题出现的概率,让程序睡眠10ms
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票,ticket--
System.out.println(Thread.currentThread().getName()
+"->"+"正在卖第"+ticket+"张票");
ticket--;
}
}
}
}
/**
* 模拟卖票案例:创建3个线程,同时开启,对共享的票进行出售
*
*/
public class Demo8Ticket {
public static void main(String[] args) {
//创建Runnable接口的实现类对象
Demo8RunnableImpl_Ticket run = new Demo8RunnableImpl_Ticket();
//创建Thread类对象(线程类对象),构造方法中传递Runnable接口的实现类对象
Thread t1 = new Thread(run);//将线程任务扔到三个线程中,三个线程交替卖票
Thread t2 = new Thread(run);
Thread t3 = new Thread(run);
//调用start方法开启多线程
t1.start();
t2.start();
t3.start();
}
}
2.3.线程安全问题产生原理
- 上述程序出现了线程安全问题,卖票出现了重复的票和不存在的票。
- 上述程序开启了3个线程t0,t1,t2,这3个线程一起抢夺cpu的执行权,谁抢到就执行谁,
- 当t0线程抢到cpu的执行权,进入到run方法中执行,执行到if语句就失去了cpu的执行权(因为sleep睡眠使得t0放弃)
- 然后t2线程抢到cpu执行权,并进入到run方法中执行,执行到if语句就失去了cpu的执行权,
- 最后t1线程抢到cpu执行权,并进入到run方法中执行,执行到if语句就失去了cpu的执行权,
- 现在三个线程都进入到if语句睡眠了,若t2睡醒了,它抢到了cpu的执行权,继续执行if中程序,进行卖票,输出Thread-2->正在卖第1张票,然后执行ticket--,ticket值变为0,t2线程结束。
- 然后t0线程睡醒了,它抢到了cpu执行权,继续执行if中程序,输出Thread-0->正在卖第0张票,然后执行ticket--,ticket值变为-1,t0线程结束。
- 然后t1线程睡醒了,它抢到cpu执行权,继续执行if中程序,输出Thread-1->正在卖第-1张票,然后执行ticket--,ticket值变为-2,t1线程结束。
- 这就出现线程安全问题,出现不存在票原因。也存在三个线程t0,t1,t2同时执行到 "正在卖第"+ticket+"张票",这时ticket还没有执行到--,因此也会出现票相同情况。
注意
- 线程安全问题是不能产生的,为此,我们可以让一个线程在访问共享数据的时候,无论是否失去了cpu的执行权,让其它的线程只能等待,等待当前的线程卖完票,其它线程再卖票。保证是一个线程在卖票,而不是三个线程高速切换卖票。
2.4.解决线程安全问题三种方式
方式一:同步代码块
格式:
synchronized(锁对象){
可能会出现的线程安全问题的代码(即访问了共享数据的代码)
}
注意:
- 1.同步代码块中的锁对象,可以使用任意的对象
- 2.但是必须保证多个线程使用的锁对象是同一个
- 3.锁对象作用:把同步代码块锁住,只让一个线程在同步代码块中执行。而同步代码块外的程序并没有被锁住,若执行代码块之外的程序时cpu被B线程抢夺了,那么当前线程就会进入阻塞,B线程会执行,这个过程不涉及锁问题;但当在同步代码块中cpu被B线程抢夺了,而当前线程只有执行完所有程序后才会交出锁对象,因此,即时B线程抢夺了cpu但没有锁对象,只能交出cpu控制权。可以参考等待唤醒案例代码。
package Thread.demo9.Synchronized;
/**
* 多线程实现卖票案例: 线程安全问题的代码实现
* 卖票案例出现了线程安全问题,卖出了不存在的票和重复的票。
*/
//1.创建Runnable接口的实现类
public class Demo8RunnableImpl_Ticket implements Runnable{
//定义一个多个线程共享的票源
private int ticket=10;
//创建一个锁对象(随意)
Object obj=new Object();
//2.重写实现类中的run方法,设置线程任务为卖票
@Override
public void run() {
//使用死循环,让卖票操作重复执行
while(true){
//同步代码块
synchronized(obj){
//先判断票是否存在
if(ticket>0){
//为提高安全问题出现的概率,让程序睡眠10ms
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票,ticket--
System.out.println(Thread.currentThread().getName()+"->"
+"正在卖第"+ticket+"张票");
ticket--;
}
}
}
}
}
package Thread.demo9.Synchronized;
import Thread.demo8.ThreadSafe.Demo8RunnableImpl_Ticket;
/**
* 模拟卖票案例:
* 创建3个线程,同时开启,对共享的票进行出售
*/
public class Demo8Ticket {
public static void main(String[] args) {
//创建Runnable接口的实现类对象
Demo8RunnableImpl_Ticket run = new Demo8RunnableImpl_Ticket();
//创建Thread类对象(线程类对象),构造方法中传递Runnable接口的实现类对象
Thread t1 = new Thread(run);//创建一个实现类传递到三个线程中,三个线程交替卖票
Thread t2 = new Thread(run);
Thread t3 = new Thread(run);
//调用start方法开启多线程
t1.start();
t2.start();
t3.start();
}
}
同步技术原理
- 使用了一个锁对象,这个锁对象叫同步锁,也叫对象锁,也叫对象监视器。
- 3个线程一起抢夺cpu执行权,谁抢到了谁就执行run方法进行卖票,
- t0线程抢到了cpu执行权,执行run方法,在run方法中遇到synchronized代码块,这时t0会检查synchronized代码块是否有锁对象,发现有,就会获取到锁对象,进入到同步中执行。 (在同步中执行时可能会失去cpu控制权被其它线程抢夺,但其他线程却因为获取不到锁对象而进入阻塞状态无法执行线程任务,当t0线程再次获取到cpu控制权后继续之前的执行,直到该线程结束才释放锁对象)。
- t1线程抢到了cpu执行权后,执行run方法,在run方法中遇到synchronized(同步)代码块后,这时t1线程会检查synchronized代码块是否有锁对象,发现没有(锁对象已被t0获取),t1就会进入到阻塞状态,会一直等待t0线程归还锁对象。一直到t0线程执行完同步中的代码后,会把锁对象归还给同步代码块, t1才能获取到锁对象进入到同步中执行。
- 总结:同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步。
- 同步保证了只有一个线程在同步中执行共享数据,保证了安全,但程序频繁判断锁,释放锁,程序效率降低。
方式二:同步方法
形式一:
- 1.使用步骤:
- 1.(在实现类中)把访问了共享数据的代码抽取出来,放到一个方法中
- 2.在方法上添加synchronized修饰符
- 3.在线程的run方法中调用该方法。
- 2.格式:定义同步方法的格式
修饰符 synchronized 返回值类型值 方法名(参数列表){
可能出现线程安全问题的代码(即访问了共享数据的代码)
}
package Thread.demo10.Synchronized;
/**
* 1.买票案例出现了线程安全问题:卖出了不存在的票和重复的票。
* 2.解决线程安全问题的第二种方式:使用同步方法。
*/
public class Demo10RunnableImpl_Ticket implements Runnable{
//定义一个多个线程共享的票源
private int ticket=30;
//重写run方法,设置线程任务为卖票
@Override
public void run() {
//使用死循环,让卖票操作重复执行
while(true){
payTicket(); //定义一个同步方法,来同步卖票(即一个人卖完后,才轮到另一个)
}
}
//定义一个同步方法,卖票(同步卖票)
public synchronized void payTicket(){
//先判断票是否存在
if(ticket>0){
//提高线程安全问题出现的概率,让程序睡眠(可不写)
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票,ticket--
System.out.println(Thread.currentThread().getName()+"->"+ticket+"号票");
ticket--;
}
}
}
package Thread.demo10.Synchronized;
/**
* 模拟卖票案例
* 创建3个线程,同时开启,对共享的票进行出售
*/
public class Demo10Ticket {
public static void main(String[] args) {
//创建Runnable接口的实现类对象
Demo10RunnableImpl_Ticket run = new Demo10RunnableImpl_Ticket();
//创建Thread类对象(线程对象),构造方法中传递Runnable接口的实现类对象
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
//调用start run方法开启多线程
t0.start();
t1.start();
t2.start();
}
}
形式二:
将抽取的代码以同步代码块形式放入方法中,方法不在使用sychronized修饰。
public class Demo10RunnableImpl_Ticket1 implements Runnable{
//定义一个多个线程共享的票源
private int ticket=10;
//重写run方法,设置线程任务为卖票
@Override
public void run() {
System.out.println("this"+this);
//使用死循环,让卖票操作重复执行
while(true){
payTicket(); //定义一个同步方法,来同步卖票(即一个人卖完后,才轮到另一个)
}
}
/**
* 定义一个方法,卖票(同步卖票)
*/
public /*synchronized*/ void payTicket(){
synchronized(this){
//先判断票是否存在
if(ticket>0){
//提高线程安全问题出现的概率,让程序睡眠(可不写)
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票,ticket--
System.out.println(Thread.currentThread().getName()+"->"+ticket+"号票");
ticket--;
}
}
}
}
/**
* 模拟卖票案例
* 创建3个线程,同时开启,对共享的票进行出售
*/
public class Demo10Ticket1 {
public static void main(String[] args) {
//创建Runnable接口的实现类对象
Demo10RunnableImpl_Ticket1 run1 = new Demo10RunnableImpl_Ticket1();
System.out.println("run:"+run1);
//创建Thread类对象(线程对象),构造方法中传递Runnable接口的实现类对象
Thread t0 = new Thread(run1);
Thread t1 = new Thread(run1);
Thread t2 = new Thread(run1);
//调用start方法开启多线程
t0.start();
t1.start();
t2.start();
}
}
.同步方法是如何保证线程安全的?
- 同步方法也会把方法内部的代码锁住,只让一个线程执行,
- 同步方法的锁对象并不是随意的,而是实现类对象即new Demo10RunnableImpl_ticket();也就是this
形式三:静态同步方法
- 即把创建多线程第一种方式的同步代码块抽取出来,放到方法中,并且这个方法不仅需要synchronized修饰还需要static修饰,这才算是静态同步方法。
- 静态同步方法的锁对象不能是this, 因为this是创建对象后产生的,而静态方法优先于对象。
- 静态同步方法的锁对象是本类的class属性即class文件对象。
/**
* 1.买票案例出现了线程安全问题:卖出了不存在的票和重复的票。
* 2.解决线程安全问题的第三种方式:使用静态同步方法。
*/
public class Demo11RunnableImpl_Ticket1 implements Runnable {
//定义一个多个线程共享的票源
private static int ticket = 10; //静态方法只能方法静态静态变量
//重写run方法,设置线程任务为卖票
@Override
public void run() {
System.out.println("this:" + this);
//使用死循环,让卖票操作重复执行
while (true) {
payTicketStatic(); //定义一个静态同步方法,来同步卖票
}
}
/**
* 定义一个静态同步方法,卖票(同步卖票)
* 静态同步方法的锁对象是本类的class属性即class文件对象。
*/
public static synchronized void payTicketStatic() {
//先判断票是否存在
if (ticket > 0) {
//提高线程安全问题出现的概率,让程序睡眠(可不写)
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票,ticket--
System.out.println(Thread.currentThread().getName() + "->"
+ ticket + "号票");
ticket--;
}
}
}
/**
* 模拟卖票案例
* 创建3个线程,同时开启,对共享的票进行出售
*/
public class Demo11Ticket1 {
public static void main(String[] args) {
//创建Runnable接口的实现类对象
Demo10RunnableImpl_Ticket1 run1 = new Demo10RunnableImpl_Ticket1();
System.out.println("run:"+run1);
//创建Thread类对象(线程对象),构造方法中传递Runnable接口的实现类对象
Thread t0 = new Thread(run1);
Thread t1 = new Thread(run1);
Thread t2 = new Thread(run1);
//调用start方法开启多线程
t0.start();
t1.start();
t2.start();
}
}
静态同步方法另一种
public class Demo12RunnableImpl_Ticket1 implements Runnable {
//定义一个多个线程共享的票源
private static int ticket = 10; //静态方法只能访问静态静态变量
//重写run方法,设置线程任务为卖票
@Override
public void run() {
System.out.println("this:" + this);
//使用死循环,让卖票操作重复执行
while (true) {
payTicketStatic(); //定义一个静态同步方法,来同步卖票
}
}
/**
* 定义一个静态方法,卖票(同步卖票)
* 静态同步方法的锁对象是本类的class属性即class文件对象。
*/
public static void payTicketStatic() {
synchronized (Demo12RunnableImpl_Ticket1.class){
//先判断票是否存在
if (ticket > 0) {
//提高线程安全问题出现的概率,让程序睡眠(可不写)
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票,ticket--
System.out.println(Thread.currentThread().getName() + "->"
+ ticket + "号票");
ticket--;
}
}
}
}
方式三:Lock(同步锁)
- jdk1.5之后出现了Lock接口:java.util.concurrent.locks.Lock
- Lock锁比synchronized代码块和synchronized方法的优势:
- Lock机制提供了比synchronized(同步)代码块和synchronized方法更广泛的锁定操作(加锁与释放锁方法化了),同步代码块/同步方法具有的 功能Lock都有,除此之外更强大,更体现面向对象。
- Lock锁也称同步锁,加锁与释放锁方法化了,如下:
- public void lock():加同步锁
- public void unlock():释放同步锁
- java.util.concurrent.locks.ReentrantLock implements Lock接口
- ReentrantLock类是Lock接口的实现类,已经默认实现,不需自己实现。
- 使用步骤:
- 1.在成员位置创建一个ReentrantLock对象
- 2.在可能会出现安全问题的代码前调用Lock接口中的lock方法获取锁
- 3.在可能会出现安全问题的代码后调用Lock接口中的unlock方法释放锁
package Thread.demo13.Synchronized;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo13RunnableImpl_Ticket1 implements Runnable {
//定义一个多个线程共享的票源
private static int ticket = 10; //静态方法只能访问静态变量
//1.在成员位置创建一个ReentrantLock对象(多态方式)
Lock l=new ReentrantLock();
//重写run方法,设置线程任务为卖票
@Override
public void run() {
System.out.println("this:" + this);
//使用死循环,让卖票操作重复执行
while (true) {
//2.在可能会出现安全问题的代码前调用Lock接口中的lock方法获取锁
l.lock();
//先判断票是否存在
if (ticket > 0) {
//提高线程安全问题出现的概率,让程序睡眠(可不写)
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//票存在,卖票,ticket--,输出当前线程名称和票号
System.out.println(Thread.currentThread().getName() + "->"
+ ticket + "号票");
ticket--;
}
//3.在可能会出现安全问题的代码后调用Lock接口中的unlock方法释放锁
l.unlock();
}
/**
* 上述Lock锁还可以换一种更好的写法:即把unlock方法放在异常的finally中,
* 这样,无论程序是否出现异常,都会把锁释放,提高程序的效率。
* while (true) {
* //2.在可能会出现安全问题的代码前调用Lock接口中的lock方法获取锁
* l.lock();
* //先判断票是否存在
* if (ticket > 0) {
* //提高线程安全问题出现的概率,让程序睡眠(可不写)
* try {
* Thread.sleep(10);
* //票存在,卖票,ticket--
* System.out.println(Thread.currentThread().getName() + "->"
+ ticket + "号票");
* ticket--;
* } catch (InterruptedException e) {
* e.printStackTrace();
* }finally{
* l.unlock();//无论程序是否出现异常,都会把锁释放,提高程序的效率
* }
* }
* }
*/
}
}