线程安全问题
线程安全问题遵循这三个规律:是否是多线程;是否有共享数据;是否有多个线程操作共享数据。
在这之前,我们也需要了解一些名词
同步(线程安全):排队执行,效率低但是安全
异步:同时执行,效率高但是数据不安全
并发:指两个或多个事件在同一个时间段内发生
并行:指两个或多个事件在同一时刻发生(同时发生)
当多个线程同时处理同一个问题时可能会出现线程不安全问题,比如三个人同时为某一影院卖票(相当于三个线程同时处理卖票这一个任务)
示例如下:
/**
* 线程不安全问题,多个线程同时处理同一问题会出现不安全问题
*/
public class Demo8 {
public static void main(String[] args) {
Runnable run = new Ticket();
//假设三个线程同时处理卖票
new Thread(run,"A").start();
new Thread(run,"B").start();
new Thread(run,"C").start();
}
static class Ticket implements Runnable{
//票数
int count = 10; //假设为十张票
@Override
public void run() {
//用一个卖票的案例说明
while (count>0){
System.out.println("正在准备卖票");
try {
//间隔1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//卖出一张票 票数count-1
count--;
System.out.println("出票成功,余票为:"+count);
}
}
}
}
运行效果如下:
你会发现这里票数会出现0和-1,这是为什么呢?因为A线程抢到执行权进来时,会休眠1秒,休眠的这段期间,另一个人抢到了执行权,也就是B,这时候B也进来了,然后它也休眠了,这个时候C苏醒啦,它就完成了票减1,票就变为1了,在B还没苏醒时A进来并休眠了,然后B苏醒了,它也进行了减1,票就变为了0,最后A醒来并减1,票就变为-1了,因此出现了以上的结果,这就体现了线程的互斥性;
上面的结果也可能出现相同票数的人,原因是ticket–不具有原子性(原子性也就是式子不是一次计算出结果的,而是分布计算结果的,如:i–,它是先输出i的值,然后在进行i=i-1,同理i++)式子,如:当A休眠时,B进入之后也休眠,这时A苏醒了,进行到输出i的值这一步时,B苏醒了并抢到了执行权,它也进行了输出i,由于上一步还没进行到i-1的步骤,因此B和A输出的值相同,这就是原子性。这些都是在网络延迟中会出现的问题。
想要解决以上问题(也就是线程安全问题),只让一个线程进入,并阻止另一个线程进入,那么就使用到了synchronize(同步代码块)或者继承Lock(锁)。
解决线程不安全问题:
方案一:同步代码块 synchronized
格式: synchronized(锁对象){
需要同步的代码
}
- 锁对象排队机制:锁对象的先执行,只看某特定的锁,若锁中有线程正 在执行
- 其他线程就不能进去,只能等待锁的对象执行完毕
对以上卖票的案例修改代码如下:
public class Demo9 {
public static void main(String[] args) {
Runnable run = new Ticket1();
//假设三个线程同时处理卖票
new Thread(run,"A").start();
new Thread(run,"B").start();
new Thread(run,"C").start();
}
static class Ticket1 implements Runnable{
//票数
private int count = 10; //假设为十张票
private Object o =new Object();
@Override
public void run() {
//用一个卖票的案例说明
while (true){
synchronized (o) {
if (count > 0) {
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//卖出一张票 票数count-1
count--;
System.out.println(Thread.currentThread().getName()+"出票成功,余票为:" + count);
} else {
break;
}
}
}
}
}
}
/**
* 这个对象其实就是一把锁,也可以称之为监视器,监视进程的进出,
* 它是同步代码块保证数据的安全性的一个主要因素
* 要注意的是这个对象,要定义为静态成员变量,
* 才能被所有线程共享,不能在里面new对象,这样每把锁都不一样了,这个锁也就没有意义了
*/
运行效果如下:
方法二:
用synchronized修饰构造方法
示例如下:
/**
* 解决方案2:同步方法
* 通过构造用synchronized修饰的方法
*/
public class Demo10 {
public static void main(String[] args) {
Runnable run = new Ticket2();
//假设三个线程同时处理卖票
new Thread(run,"A").start();
new Thread(run,"B").start();
new Thread(run,"C").start();
}
static class Ticket2 implements Runnable{
//票数
private int count = 10; //假设为十张票
private Object o =new Object();
@Override
public void run() {
//用一个卖票的案例说明
while (true){
boolean flag = sale();
if (!flag){
break;
}
}
}
public synchronized boolean sale(){
while (true){
synchronized (o) {
if (count > 0) {
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//卖出一张票 票数count-1
count--;
System.out.println(Thread.currentThread().getName()+"出票成功,余票为:" + count);
return true;
}
return false;
}
}
}
}
}
运行效果如上
方法三:
显示锁 Lock类
用同步代码块我们可以给线程上锁,但是具体在哪里上锁和解锁,我们都不知道,因此为了让我们更加清楚如何加锁和解锁,在JDK5以后提供了Lock接口和它的ReentrantLock子类,这个锁与synchronized不同,需要手动去加锁和解锁
项目 | Value |
---|---|
void lock() | 加锁 |
void unlo() | 解锁 |
示例如下:
/**
* 解决方案3:显示锁 ,Lock类
* 显示锁与隐式锁的区别
*/
public class Demo11 {
public static void main(String[] args) {
Runnable run = new Ticket3();
//假设三个线程同时处理卖票
new Thread(run,"A").start();
new Thread(run,"B").start();
new Thread(run,"C").start();
}
static class Ticket3 implements Runnable{
//票数
int count = 10; //假设为十张票
//显示锁 子类ReentrantLock,若fair值传tru就是公平锁
//公平锁:谁先来,谁就可以先执行
private Lock l = new ReentrantLock(true);
@Override
public void run() {
//用一个卖票的案例说明
while (true){
//上锁
l.lock();
if (count>0) {
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//卖出一张票 票数count-1
count--;
System.out.println(Thread.currentThread().getName()+"出票成功,余票为:" + count);
}else {
break;
}
//开锁
l.unlock();
}
}
}
}
运行效果如上
死锁问题:
死锁是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象,这个问题和同步代码块的嵌套有关,如果出现了同步嵌套,就容易产生死锁问题
示例如下:
假如两个人只有一把钥匙,要是一个人拿走了钥匙去开门,另外一个人就得等待已经拿走钥匙的人开门
/**
* 死锁问题
* 假如两个人只有一把钥匙,要是一个人拿走了钥匙去开门,
* 另外一个人就得等待已经拿走钥匙的人开门
*/
public class Demo15 {
public static void main(String[] args) {
Person p1 = new Person(true);
Person p2= new Person(false);
p1.start();
p2.start();
}
//继承Thread,重写run方法
static class Person extends Thread {
//定义一个布尔类型来,判断是谁先拿锁,true是1先拿锁,反之是2
boolean flag;
public Person(boolean flag){
this.flag=flag;
}
@Override
public void run() {
if(flag){
//嵌套同步代码块
synchronized (MyLock.key1){
System.out.println("门开了1");
synchronized (MyLock.key2){
System.out.println("门开了2");
}
}
}else {
synchronized (MyLock.key2){
System.out.println("门开了2");
synchronized (MyLock.key1){
System.out.println("门开了1");
}
}
}
}
}
static interface MyLock {
Object key1 = new Object() ;
Object key2 = new Object() ;
}
}
运行效果如图:
当是true时,由于1先拿了钥匙,还没释放,2就拿不到钥匙,因此就会出现一直等待的现象,这就是死锁
线程的六种状态
Java中线程的状态分为6种。
1.初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
3. 阻塞(BLOCKED):表示线程阻塞于锁。
4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
6. 终止(TERMINATED):表示该线程已经执行完毕。
线程的状态图:
二、状态详细说明
- 初始状态(NEW)
实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。
2.1. 就绪状态(RUNNABLE之READY)
就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
调用线程的start()方法,此线程进入就绪状态。
当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
锁池里的线程拿到对象锁后,进入就绪状态。
2.2. 运行中状态(RUNNABLE之RUNNING)
线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一的一种方式。
-
阻塞状态(BLOCKED)
阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。 -
等待(WAITING)
处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。 -
超时等待(TIMED_WAITING)
处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。 -
终止状态(TERMINATED)
当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常
线程池
线程池 Executors
线程池的好处
降低资源消耗。
提高响应速度。
提高线程的可管理性。
Java中的四种线程池 . ExecutorService
- 缓存线程池‘
public class Demo16 {
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
//向线程池中加入新的任务
service.execute(new Runnable() {
//匿名内部类
@Override
public void run() {
System.out.println("线程的名字:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
//匿名内部类
@Override
public void run() {
System.out.println("线程的名字"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
//匿名内部类
@Override
public void run() {
System.out.println("线程的名字"+Thread.currentThread().getName());
}
});
/* try {
//让主线程休眠一秒钟
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
*/
}
}
运行效果如上图
2、定长线程池
/**
* 定长线程池.
* (长度是指定的数值)
* 执行流程:
* 1. 判断线程池是否存在空闲线程
* 2. 存在则使用
* 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
* 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
*/
public class Demo17 {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(new Runnable() {
@Override public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
}
}
运行效果如下
3、单线程线程池
效果与定长线程池,创建时传入数值1的效果一样
/**
* 单线程线程池.
* 执行流程:
* 1. 判断线程池 的那个线程 是否空闲
* 2. 空闲则使用
* 3. 不空闲,则等待 池中的单个线程空闲后 使用
*/
public class Demo18 {
public static void main(String[] args) {
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:" + Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:" + Thread.currentThread().getName());
}
});
}
}
4、周期性任务定长线程池
/**
*周期任务 定长线程池.
* 执行流程:
* 1. 判断线程池是否存在空闲线程
* 2. 存在则使用
* 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
* 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
* 周期性任务执行时:
* 定时执行, 当某个时机触发时, 自动执行某任务
*/
public class Demo19 {
public static void main(String[] args) {
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
/**定时执行
* 参数1. runnable类型的任务
* 参数2. 时长数字
* 参数3. 时长数字的单位
*/
/*service.schedule(new Runnable() {
@Override public void run() {
System.out.println("俩人相视一笑~ 嘿嘿嘿");
}
},5,TimeUnit.SECONDS); */
/**
* 周期执行
* 参数1. runnable类型的任务
* 参数2. 时长数字(延迟执行的时长)
* 参数3. 周期时长(每次执行的间隔时间)
* 参数4. 时长数字的单位
*/
service.scheduleAtFixedRate(new Runnable() {
@Override public void run() {
System.out.println("俩人相视一笑~ 嘿嘿嘿");
}
},5,2, TimeUnit.SECONDS);
}
}
运行效果如下