线程安全问题的概述:
单线程程序是不会出现线程安全问题的。
多线程程序没有访问共享数据,不会产生问题。
多线程访问了共享的数据,会产生线程安全问题。
线程安全问题的代码实现:
/* * 实现卖票案例 * */ public class RunnableImpl implements Runnable { //定义一个多个线程共享的票源 private int ticket=100; //设置线程任务:买票 @Override public void run() { //使用死循环,让卖票操作重复执行 while(true){ //提高安全问题出现的概率,让程序睡眠 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } //先判断票是否存在 if(ticket>0){ //票存在,卖票ticket-1 System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票"); ticket--; } } } } /* * 模拟卖票案例 * 创建三个线程,同时开启,对共享的票进行出售 * */ public class Demo01Ticket { public static void main(String[] args) { //创建Runnable接口的实现类对象 RunnableImpl run=new RunnableImpl(); //创建Thread类对象,构造方法中传递Runnable接口的实现类对象 Thread t0=new Thread(run); Thread t1=new Thread(run); Thread t2=new Thread(run); //调用start方法开启多线程 t0.start(); t1.start(); t2.start(); } }
执行结果:
Thread-2正在卖第100张票
Thread-0正在卖第100张票
Thread-1正在卖第100张票
Thread-1正在卖第97张票
Thread-2正在卖第97张票
Thread-0正在卖第97张票
Thread-2正在卖第94张票
Thread-1正在卖第94张票
...
Thread-0正在卖第4张票
Thread-1正在卖第3张票
Thread-2正在卖第2张票
Thread-2正在卖第1张票
Thread-1正在卖第0张票
Thread-0正在卖第-1张票
线程安全问题产生的原理:
3个线程一起抢夺cpu的执行权,谁抢到谁执行
(1)t2线程抢到了cpu的执行权,进入到run方法中执行
执行到if语句,就失去了cpu的执行权。t2睡醒了,抢到cpu的执行权,继续执行进行卖票
Thread-2正在卖第1张票 ticket-- ticket=0 继续判断0>0不执行
(2)t0线程抢到了cpu的执行权,进入到run方法中执行
执行到if语句,就失去了cpu的执行权。t0睡醒了,抢到cpu的执行权,继续执行进行卖票
Thread-0正在卖第-1张票 ticket-- ticket=-2 继续判断-2>0不执行
(3)t1线程抢到了cpu的执行权,进入到run方法中执行
执行到if语句,就失去了cpu的执行权。t1睡醒了,抢到cpu的执行权,继续执行进行卖票
Thread-1正在卖第0张票 ticket-- ticket=-1 继续判断-1>0 不执行
Thread-2正在卖第100张票
Thread-0正在卖第100张票
Thread-1正在卖第100张票
t2,t0,t1同时执行到了“正在卖”+ticket+"张票"
这时ticket还没有执行到--
注意:线程安全问题是不能产生的,我们可以让一个线程在访问共享数据时,无论是否失去了cpu的执行权,让其他的线程只能等待,等待当前线程卖完票,其他线程再进行卖票
保证:使用一个线程在卖票。
解决线程安全问题_同步代码块:
/* * 卖票案例出现了线程安全问题 * 卖出了不存在的票和重复的票 * 解决线程安全问题的第一种方案:使用同步代码块 * 格式: * synchronized(锁对象){ * 可能会出现线程安全问题的代码(访问了共享数据的代码) * } * 注意: * 1.通过代码块中的锁对象,可以使用任意的对象 * 2.但是必须保证多个线程使用的锁对象是同一个 * 3.锁对象作用: * 把同步代码块锁住,只让一个线程在同步代码块中执行 * */ public class RunnableImpl implements Runnable { //定义一个多个线程共享的票源 private int ticket=100; //设置线程任务:买票 //创建一个锁对象 Object obj=new Object(); @Override public void run() { //使用死循环,让卖票操作重复执行 while(true){ //创建同步代码块 synchronized (obj){ //提高安全问题出现的概率,让程序睡眠 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } //先判断票是否存在 if(ticket>0){ //票存在,卖票ticket-1 System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票"); ticket--; } } } } }
同步技术的原理:
使用了一个锁对象,这个锁对象叫同步锁,也叫对象锁,也叫对象监视器
3个线程一起抢夺cpu的执行权,谁抢到了谁执行run方法进行卖票
t0抢到了cpu的执行权,执行了run方法,遇到synchronized代码块,这时t0会检查synchronized代码块是否有锁对象,发现有,就会获取到锁对象,进入到同步中执行
t1抢到了cpu的执行权,执行了run方法,遇到synchronized代码块,这时t1会检查synchronized代码块是否有锁对象,发现没有,t1就会进入到阻塞状态,会一直等待t0线程归还锁对象;一直等到t0线程执行完同步中的代码,会把锁对象归还给同步代码块,t1才能获取到锁对象进入到同步中执行
总结:同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步
同步保证了只能有一个线程在同步中执行共享数据,保证了安全
程序频繁的判断锁,获取锁,释放锁,程序的效率会降低。
解决线程安全问题_同步方法:
/* * 卖票案例出现了线程安全问题 * 卖出了不存在的票和重复的票 * 解决线程安全问题的第二种方案:使用同步方法 * 使用步骤: * 1.把访问了共享数据的代码抽取出来,放到一个方法中 * 2.在方法上添加synchronized修饰符 * 格式:定义方法的格式 * 修饰符 synchronize 返回值类型 方法名(参数列表){ * 可能会出现线程安全问题的代码(访问了共享数据的代码) * } * */ public class RunnableImpl implements Runnable { //定义一个多个线程共享的票源 private int ticket=100; //设置线程任务:买票 @Override public void run() { //使用死循环,让卖票操作重复执行 while(true){ payTicket(); } } /* * 定义一个同步方法 * 同步方法也会把方法内部的代码锁住,只让一个线程执行 * 同步方法的锁对象是实现类对象 new RunnableImpl() * 也就是this * */ public synchronized void payTicket(){ //先判断票是否存在 if(ticket>0){ //票存在,卖票ticket-1 System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票"); ticket--; } } }
静态同步方法(了解):
public class RunnableImpl implements Runnable { //定义一个多个线程共享的票源 private static int ticket=100; //设置线程任务:买票 @Override public void run() { //使用死循环,让卖票操作重复执行 while(true){ payTicketStatic(); } } /* * 静态的同步方法,锁对象不是this * this是创建对象之后产生的,静态方法优先于对象 * 静态方法的锁对象是本类的class属性--》class文件对象(反射部分) * */ public static synchronized void payTicketStatic(){ //先判断票是否存在 if(ticket>0){ //票存在,卖票ticket-1 System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票"); ticket--; } } }
解决线程安全问题_Lock锁:
/* * 卖票案例出现了线程安全问题 * 卖出了不存在的票和重复的票 * 解决线程安全问题的第三种方案:使用Lock锁 * java.util.concurrent.locks.Lock接口 * Lock实现提供比使用synchronized方法和语句可以获得的更广泛的锁定操作。 * Lock接口中的方法: * void lock() 获取锁 * void unlock() 释放锁 * java.util.concurrent.locks.ReentrantLock implements Lock接口 * 使用步骤: * 1.在成员位置创建一个ReentrantLock对象 * 2.在可能出现安全问题的代码前调用Lock接口中的方法lock获取锁 * 3.在可能出现安全问题的代码后调用Lock接口中的方法unlock释放锁 * */ public class RunnableImpl implements Runnable { //定义一个多个线程共享的票源 private int ticket=100; //1.在成员位置创建一个ReentrantLock对象 Lock l=new ReentrantLock(); //设置线程任务:买票 @Override public void run() { //使用死循环,让卖票操作重复执行 while(true){ l.lock(); //提高安全问题出现的概率,让程序睡眠 if(ticket>0){ try { Thread.sleep(10); System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票"); ticket--; } catch (InterruptedException e) { e.printStackTrace(); }finally { l.unlock();//无论程序是否异常,都会把锁释放 } } } } /*@Override public void run() { //使用死循环,让卖票操作重复执行 while(true){ //2.在可能出现安全问题的代码前调用Lock接口中的方法lock获取锁 l.lock(); //先判断票是否存在 if(ticket>0){ //票存在,卖票ticket-1 System.out.println(Thread.currentThread().getName()+"正在卖第"+ticket+"张票"); ticket--; } //3.在可能出现安全问题的代码后调用Lock接口中的方法unlock释放锁 l.unlock(); } }*/ }