线程安全
1、什么是线程安全问题?
当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题,但是如果多个线程中对资源有读和写的操作,就容易出现线程安全问题。
如下:
public class TestSafe {
public static void main(String[] args) {
SaleTicketThread s1 = new SaleTicketThread("窗口1");
SaleTicketThread s2 = new SaleTicketThread("窗口2");
SaleTicketThread s3 = new SaleTicketThread("窗口3");
s1.start();
s2.start();
s3.start();
//代码运行出现了问题:负数票或重复票
}
}
class SaleTicketThread extends Thread{
private static int total = 10;
public SaleTicketThread(String name) {
super(name);
}
public void run(){
while(total>0){
try {
Thread.sleep(10);//让程序慢一点,也是为了让问题暴露的更明显
} catch (InterruptedException e) {
e.printStackTrace();
}
total--;
System.out.println(getName() + "卖出一张票,剩余:" + total + "张");
}
}
}
造成上述结果的原因如图所示:
2 如何解决线程安全问题?
思路:对影响共享数据的代码加“锁”,一个线程未执行完之前,其他的线程就只能等待。
如何加锁?
JSE阶段只介绍同步锁,synchronized
3.同步的语法
(1)同步代码块
(2)同步方法
4.同步代码块
synchronized(锁对象){
代码
}
锁对象又称为监视器对象(门卫)。
注意:
(1)锁对象的选择问题
(2)锁的代码范围问题
把影响和访问共享数据的代码都要锁进去
5.锁对象有什么要求?
(1)什么类型的对象可以作为锁对象?
任意引用数据类型的对象都可以。
(2)锁对象的要求
必须多个具有竞争关系的线程,要使用“同一个”锁对象。(同一个是指内存地址一样)
把买票的例子,代码重新处理一下:
①:资源类
买票的这个例子中,票是资源
②:线程类
模拟卖票的窗口
③测试类
创建线程对象,资源对象等,并且启动线程
public class TestSafe2 {
public static void main(String[] args) {
Ticket ticket = new Ticket(1000);
//分为三个窗口同时卖票
Window w1 = new Window("窗口1",ticket);
Window w2 = new Window("窗口2",ticket);
Window w3 = new Window("窗口3",ticket);
//启动线程
w1.start();
w2.start();
w3.start();
}
}
class Window extends Thread{
private Ticket ticket; //如果是同一个ticket对象,那么就是同一个“电影”的票
public Window(String name, Ticket ticket) {
super(name);
this.ticket = ticket;
}
public void run(){
// synchronized (this) {//(1)这里的this是Window类型(2)三个线程使用的是同一个Window对象 ==>这里不能用this
//synchronized (ticket) {//(1)这里的ticket是Ticket类型(2)三个线程使用的是同一个ticket
//把while (ticket.getTotal() > 0) 锁进去,范围又太大了,导致其他线程没机会了
/*while (ticket.getTotal() > 0) {
synchronized (ticket){ //这样又范围太小了, ticket.getTotal() > 0没锁进来
try {
Thread.sleep(10);//模拟两次卖票之间的时间间隔
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket.sale();
}
}*/
while(true){
synchronized (ticket){
if(ticket.getTotal()>0) {
try {
Thread.sleep(10);//模拟两次卖票之间的时间间隔
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket.sale();
}else{
break;
}
}
}
}
}
class Ticket{
//票有很多信息,编号等,这里暂时忽略,我们只关注数量
private int total;
public Ticket(int total) {
this.total = total;
}
public int getTotal() {
return total;
}
public void sale(){
//synchronized (this) {//(1)这里this是Ticket类型(2)3个卖票的线程,使用的this对象是否是同一个
//锁这里范围太小
total--;
System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余:" + total + "张");
// }
}
}
6 为什么任意类型的对象都可以当锁?而且为什么要求是同一个对象?
因为Java对象在内存中存储时分为三个部分:
(1)对象头
①当前对象所属的类型
②对象的hashcode值
③线程id-----哪个线程现在占有锁对象,只有锁对象中标记的线程具有执行同步代码块的权力,其他线程只能等待
④...其他
(2)对象的实例对象
(3)对齐空白
7.同步方法:静态方法
语法格式:
【其他修饰符】 synchronized 返回值类型 方法名(【形参列表】)【throws 异常列表】{
}
无论是同步代码块还是同步方法,好不好用,能不能解决线程安全问题,都是从两个方面来说:
(1)锁对象的选择
同步方法的锁对象是默认。
非静态方法,默认就是this对象。
静态方法,默认就是当前类的Class对象。
对象的类型以及多个线程是否用同一个对象。
(2)锁的范围问题
把影响和访问共享数据的代码都要锁进去。
public class TestSale3 {
public static void main(String[] args) {
Piao piao = new Piao(1000);
MaiPiao m1 = new MaiPiao("窗口1",piao);
MaiPiao m2 = new MaiPiao("窗口2",piao);
MaiPiao m3 = new MaiPiao("窗口3",piao);
m1.start();
m2.start();
m3.start();
}
}
class MaiPiao extends Thread{
private static Piao piao;
/*
在讲类初始化和实例初始化。
类初始化:给静态变量初始化的。只和静态代码块和静态变量显式赋值有关。
实例初始化:给非静态变量初始化。 和构造器、非静态代码块、非静态变量显式赋值、super()和super(xx)有关。
静态变量正常是不会通过构造器进行初始化的。
*/
public MaiPiao(String name,Piao piao) {
super(name);
this.piao = piao;
}
/*
run()是非静态,默认锁对象是this
(1)这里的this对象的类型是MaiPiao
(2)这里的this,三个线程用的是同一个吗?不是
不合适
*/
/*public synchronized void run(){
while(true){
if(piao.getTotal()>0){//和共享数据有关
piao.sale();
}else{
break;
}
}
}*/
public void run(){
while(true){
if(piao.getTotal()>0) {//双重条件 同步方法里也有该条件判断,所以仍然是安全的
saleOneTicket();
}else{
break;
}
}
}
/*
saleOneTicket()是静态,默认锁对象是当前类的Class对象
(1)这里的当前类的Class对象就是MaiPiao的Class对象,
(2)这里的Class对象,三个卖票线程用的是同一个吗?是,只要是同一个类,Class对象就是同一个
Class对象是什么?
我们想一想,所有的Java类有共同特征,
都有5大成员,都能new对象...
类的定义:一类具有相同特性的事物的抽象描述。
那么所有的Java类具有共同特征,那么可以用一个类(Class)来描述类。
Class类的一个对象,就是一个具体的类,比如是Piao类,MaiPiao类,Student类。
Java中把某个类加载在内存之后,会用一个Class对象进行表示。
即只要用的是同一个类,那么就是同一个Class对象。
*/
public synchronized static void saleOneTicket(){
if(piao.getTotal()>0){//和共享数据有关
piao.sale();
}
}
}
class Piao{
//票有很多信息,编号等,这里暂时忽略,我们只关注数量
private int total;
public Piao(int total) {
this.total = total;
}
public int getTotal() {
return total;
}
/*
非静态方法,默认锁对象是this
this:(1)类型是Piao类型(2)多个线程是否用了同一个piao对象,这里是
范围太小
*/
/* public synchronized void sale(){
total--;
System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余:" + total + "张");
}*/
public void sale(){
total--;
System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余:" + total + "张");
}
}
8 同步方法:非静态方法
(1)锁对象:this,非静态方法,默认就是this
①:类型:对象的所属类
②:多个线程用同一个锁对象
(2)范围:通常情况下,就是选一次业务逻辑代码
public class TestSale4 {
public static void main(String[] args) {
CinemaTicket c = new CinemaTicket(1000);
CinemaWindow w1 = new CinemaWindow("窗口1",c);
CinemaWindow w2 = new CinemaWindow("窗口2",c);
CinemaWindow w3 = new CinemaWindow("窗口3",c);
w1.start();
w2.start();
w3.start();
}
}
//线程类
class CinemaWindow extends Thread{
private CinemaTicket ticket;
public CinemaWindow(String name, CinemaTicket ticket) {
super(name);
this.ticket = ticket;
}
public void run(){
while(true){
if(ticket.getTotal()>0){
ticket.sale();
}else{
break;
}
}
}
}
//资源类对象
class CinemaTicket {
private int total;
public CinemaTicket(int total) {
this.total = total;
}
public int getTotal() {
return total;
}
/*
(1)锁对象:this,非静态方法,默认就是this
A:类型:CinemaTicket
B:多个线程是否用同一个锁对象,是
(2)范围:通常情况下,就是选一次业务逻辑代码
例如:卖票,从查询是否还有票,到卖一张票完成,是一次业务逻辑
生产xx东西,产品比较复杂,需要组装,一次业务逻辑,就是包含,不同零件的生产,到组装完成
要求,两个线程,分别打印数字,一个线程连续打印5个数字之后,才能换另一个线程。 一次业务逻辑就是打印5个数字。
*/
public synchronized void sale(){
if(total>0) {
total--;
System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余:" + total + "张");
}else{
// System.out.println("没票了");
throw new RuntimeException("没票了");
}
}
}
单例模式
1.什么是单例设计模式?
(1)什么是单例?
单例:就是唯一的对象,即某个类在整个系统中只有唯一的对象。
(2)什么是设计模式
设计模式可以理解为一套经验。之前的很多程序员,在遇到一些问题时,采用某个代码结构,发现这个代码结构很好用,就把这些好用的代码结构总结出来,后面的程序员就可以套用。
如:
Spring是管理所有JavaBean(Java类)的容器在整个系统中只有一个。数据库连接池是用来管理,Java连接数据库的连接对象,这个池在整个系统中只有一个。
2.如何实现单例模式?
(1)饿汉式
(2)懒汉式
如何区别是饿汉式还是懒汉式?
对象的创建时间:
饿汉式在类加载和初始化时,就创建了唯一的对象,不管这个对象现在用不用,都会直接把它创建出来。
饿汉式:后期用的时候方便,直接拿来用就好了,不用当场创建
懒汉式:在使用的时候再创建。
懒汉式:可以节省类初始化的时间,减少对象占用内存的时间。
3.饿汉式实现方式
(1)enum声明的枚举实现
enum Singleton1{
INSTANCE;
}
(2)类似jdk1.5之前的枚举代码改装实现
class SingleTwo{
public static final SingleTwo INSTANCE = new SingleTwo();//INSTANCE是SingleTwo的唯一对象
private SingleTwo(){
}
(3)对(2)的改装
class SingleThree{
private static final SingleThree INSTANCE = new SingleThree();
//INSTANCE是SingleTwo的唯一对象
private SingleThree(){ }
public static SingleThree getInstance() {
return INSTANCE;
}
}
核心类库中的Runtime类就是(3)形式,
java.lang.Runtime类,它是代表当前JVM运行环境的对象。
4.懒汉式的实现方式(需要保证线程安全)
(1)同步方法
class SingleFour{
private static SingleFour instance;
private SingleFour(){}
/* synchronized方法行不行?
(1)锁对象: 静态方法的锁对象是当前类的Class对象 SingleFour的Class对象只有一个
(2)锁范围: 一次业务逻辑,判断对象new没new,没new的话,就把对象new 对象
*/
public static synchronized SingleFour getInstance(){// return new SingleFour();//错误的,这样的话,调用一次getInstance(),就new一个,就不是单例了
if(instance == null){
instance = new SingleFour();
}
return instance;
}
}
(2)同步代码块
优于同步方法,因为同步方法将synchronized加在了方法上,也就是说即使已经被创建出对象了,还是要等待这个锁释放,效率比较低
class SingleFive{
private static SingleFive instance;
private static final Object lock = new Object();
private SingleFive(){ }
public static SingleFive getInstance() {//
synchronized (this){//静态方法中,不允许出现this//synchronized (""){//""它是一个对象,空字符串对象//
synchronized (SingleFive.class){//当前类的Class对象
if(instance == null) {//提高效率
synchronized (lock) {
if (instance == null) {//保证唯一性
instance = new SingleFive();
}
}
}
return instance;
}
}
(3)静态内部类
内部类和外部类可以互访private成员,但是静态内部类不随外部类的加载而加载,它的静态成员也就不能马上被创建。这个静态内部类必须要被调用才能加载,也就是将INSTANCE对象实例化。也就完成了懒汉单例模式的实现。
class SingleSix{
private SingleSix(){} //静态内部类
private static class Inner
static final SingleSix INSTANCE = new SingleSix();
}
public static SingleSix getInstance(){
return Inner.INSTANCE;
}
}
5 单例模式的线程安全性讨论
饿汉式实现单例模式是线程安全的
懒汉式实现单例模式可能是线程不安全的。需要进行加锁的操作。
线程的等待唤醒机制
1.什么情况下会用到等待唤醒机制?
问题的场景:用来解决“生产者与消费者问题”。
一些线程负责“生产”数据,一些线程负责“消耗”数据。如果这个数据是放到某个容器中(缓冲区),缓冲区是有大小限制的。这个过程中,生产的线程和消费的线程会有这样的情况,当数据是空的时候,消费者线程是不能消费,只能“等待”,消费者消费了数据后,可以“唤醒”生产者线程。当数据是满的时候,生产者线程是不能生成,只能“等待”,生产者生产了数据后,可以“唤醒"消费者线程。
2 如何实现等待唤醒机制?
它依赖于两个方法:
(1)wait方法:等待
(2)notify方法/notifyAll方法:通知/唤醒。
这两组方法在java.lang.Object类中
3.为什么这两个和线程有关系的方法,它不在Thread类中声明,反而在Object类声明?
wait方法和notify方法/notifyAll方法,必须由”锁“对象调用,否则会报java.lang.IllegalMonitorStateException(运行时异常)异常。
4 为什么生产者消费者问题,也要锁对象?
因为生产者消费者问题也有线程安全问题。
生产者线程会读和写数据,消费者线程会读和写数据,多个线程都会对某个共享数据进行读和写,就会有线程安全问题。
5 为什么wait方法和notify/notifyAll方法必须由”锁“对象调用?
因为wait方法需要释放锁,必须告诉锁对象,需要切换线程id标识了,所以必须要”锁“对象。
6 单个消费者与多个消费者问题
示例
有家餐馆的取餐口比较小,只能放10份快餐,厨师做完快餐放在取餐口的工作台上,服务员从这个工作台取出快餐给顾客。现在有1个厨师和1个服务员。
public class TestWaitNotify {
public static void main(String[] args) {
//创建工作台
WorkBench w = new WorkBench();
//创建厨师和服务员线程,并且启动
Waiter waiter = new Waiter("翠花",w);
Cook cook = new Cook("小高",w);
waiter.start();
cook.start();
}
}
//工作台类
class WorkBench{
private int total;//记录放在工作台上的快餐的数量
private static final int MAX_VALUE = 10;
//put方法是非静态方法,默认的锁对象就是this
public synchronized void put(){
try {
if(total >= MAX_VALUE){
this.wait(); //this是WorkBench类型,它也有这个wait方法,只要是Object的子类都有
}
} catch (InterruptedException e) {
e.printStackTrace();
}
total++;
System.out.println(Thread.currentThread().getName() + "制作了一份快餐,工作台上的快餐数量是:" + total);
this.notify();
}
public synchronized void take(){
try {
if(total <= 0){
this.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
total--;
System.out.println(Thread.currentThread().getName() + "取走了一份快餐,工作台上的快餐数量是:" + total);
this.notify();
}
}
//服务员线程
class Waiter extends Thread{
private WorkBench workBench;
public Waiter(String name, WorkBench workBench) {
super(name);
this.workBench = workBench;
}
public void run(){
while(true){
workBench.take();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//厨师线程
class Cook extends Thread{
private WorkBench workBench;
public Cook(String name, WorkBench workBench) {
super(name);
this.workBench = workBench;
}
public void run(){
while(true){
workBench.put();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
7 什么时候会报IllegalMonitorStateException?
IllegalMonitorStateException:非法监视器状态异常。
因为wait和notify等方法不是由“锁”对象调用的,就会报这个异常。
8 多个生产者与多个消费者问题
(1)问题升级
饭馆扩大了,有多个服务员和多个厨师。
(2)出现了新的问题以及解决方案
①出现了-1?
Ⅰ 思路一,唤醒对方,生产者只唤醒消费者,消费者只唤醒生产者,就可以,SE阶段不行。高级juc可以。
Ⅱ 思路二:如果被唤醒之后不着急往下执行 --和++等代码,而是重新判断 条件,就可以了。 使用循环
②程序卡住了?(线程全部休眠)
Ⅰ 思路一每次唤醒时,不要只唤醒一个,唤醒所有等待的线程,让这几个线程自己抢对象锁。可以使用notifyAll方法代替notify方法
Ⅱ 不要让线程无限等待,可以设置等待时间。wait()可以换成wait(时间)等一段时间之后,如果没人唤醒,自动醒来。
public class TestWaitNotifyAll {
public static void main(String[] args) {
//创建工作台
WorkBench w = new WorkBench();
//创建厨师和服务员线程,并且启动
Waiter waiter1 = new Waiter("翠花",w);
Cook cook1 = new Cook("小高",w);
Waiter waiter2 = new Waiter("如花",w);
Cook cook2 = new Cook("小丁",w);
waiter1.start();
cook1.start();
waiter2.start();
cook2.start();
}
}
//工作台类
class WorkBench{
private int total;//记录放在工作台上的快餐的数量
private static final int MAX_VALUE = 1;//这里用1,是为了让问题及早出现
//put方法是非静态方法,默认的锁对象就是this
public synchronized void put(){
try {
// if(total >= MAX_VALUE){
while(total >= MAX_VALUE){
this.wait(); //this是WorkBench类型,它也有这个wait方法,只要是Object的子类都有
}
} catch (InterruptedException e) {
e.printStackTrace();
}
total++;
System.out.println(Thread.currentThread().getName() + "制作了一份快餐,工作台上的快餐数量是:" + total);
// this.notify();
this.notifyAll();
}
public synchronized void take(){
try {
// if(total <= 0){
while (total <= 0){
this.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
total--;
System.out.println(Thread.currentThread().getName() + "取走了一份快餐,工作台上的快餐数量是:" + total);
// this.notify();
this.notifyAll();
}
}
//服务员线程
class Waiter extends Thread{
private WorkBench workBench;
public Waiter(String name, WorkBench workBench) {
super(name);
this.workBench = workBench;
}
public void run(){
while(true){
workBench.take();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//厨师线程
class Cook extends Thread {
private WorkBench workBench;
public Cook(String name, WorkBench workBench) {
super(name);
this.workBench = workBench;
}
public void run() {
while (true) {
workBench.put();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
(3)关于上述问题的分析
①出现-1的情况
②出现全部休眠的情况
JVM的运行时内存:
(1)方法区
(2)堆
(3)Java虚拟机栈
(4)本地方法栈
(5)程序计数器:每一个线程都会在程序计数器中有一小块独立的内存,记录这个线程的下一条指令。
线程的生命周期
1.JDK1.5之前
(1)新建:new
(2)就绪:从新建-->就绪,经过了start(启动)
只有就绪状态的线程,才能被CPU调度。
(3)运行:正在被CPU调度的程序。
运行时间是非常短的,CPU分配的时间片到了,当前线程会从运行状态切换到就绪状态。从运行状态要切换到就绪状态等时,会对当前线程进行“快照”,记录当前线程运行到哪里,下次在被调用时,从那里接着运行。这个记录是由程序计数器来负责的。
线程运行到yield方法,时间片未到也会从运行状态到就绪状态。
(4)阻塞:当线程运行时,遇到了一些特殊的情况,会让线程从运行状态转入阻塞状态。
特殊情况有:
①sleep
②wait
③IO阻塞操作等比较耗时的指令
④join(加塞),其他线程执行加塞
⑤等待锁
⑥suspend(已过时)
从阻塞状态转为就绪状态:
①sleep --->sleep时间到就自动转入就绪状态
②wait --->wait时间到,或被notify/notifyAll
③IO阻塞操作等比较耗时的指令 ---->需要的数据准备好了
④join(加塞),其他线程执行加塞 --->join时间到或者加塞的线程执行完了。
⑤等待锁 --->占用锁的线程释放了锁
⑥suspend(已过时) --->resume(已过时)
(5)死亡:
正常死亡:run方法结束
非正常死亡:线程执行过程中抛出了一个未捕获的异常(Exception)或错误(Error)。
手动停止:调用该线程的stop()来结束该线程(已过时,因为容易发生死锁)
2 JDK1.5之后
java.lang.Thread类把线程状态划分的更加详细,在Thread类中,用一个内部枚举类State规定了线程的几种状态。
State枚举类有6个常量对象:
NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED
(新建,可运行,阻塞,等待,限时等待,终止)
Runnable(可运行):就是就绪和运行状态,因为这两个之间很频繁,而且运行的时间非常短,所以就没有在Java线程对象中进行区分。
BLOCKED:等待锁。
WAITING:
wait没有指定时间
join没有指定时间
LockSupport类的park方法
TIMED_WAITING:
sleep();
指定了时间的wait
指定了时间的join
当从WAITING或TIMED_WAITING恢复到Runnable状态时,如果发现当前线程没有得到监视器锁,那么会立刻转入BLOCKED状态。
TERMINATED
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
释放锁操作与死锁
1.释放锁的操作
(1)同步方法或同步代码块{}中的内容运行完了,自动释放锁。
(2)当前线程在同步代码块或同步方法出现了未处理的Error或者Exception,导致当前方法结束,自动释放锁。
(3)在同步代码块、同步方法执行了锁对象的wait()方法,当前线程被挂起,释放锁。
2.哪些操作不会释放锁?
(1)在同步代码块或者同步方法中遇到调用,Thread.sleep(),Thread.yield()方法
(2)调用了该线程的suspend()方法(已过时)
3.什么是死锁?
当两个或者多个线程,互相等待对方释放它需要的锁时,就会导致死锁。
当出现同步代码块或者同步方法嵌套使用时,就很容易出现死锁,所以要格外注意。
(1)sleep()不释放锁,wait()释放锁
(2)sleep()指定休眠的时间,wait()可以指定时间也可以无限等待直到notify或notifyAll
(3)sleep()在Thread类中声明的静态方法,wait方法在Object类中声明
因为我们调用wait()方法是由锁对象调用,而锁对象的类型是任意类型的对象。那么希望任意类型的对象都要有的方法,只能声明在Object类中。
public class TestDeadLock {
public static void main(String[] args) {
Object girlFriend = new Object();
Object money = new Object();
BoyFriend boy = new BoyFriend(girlFriend, money);
BangFei bang = new BangFei(girlFriend, money);
boy.start();
bang.start();
}
}
class BoyFriend extends Thread{
private Object girlFriend;
private Object money;
public BoyFriend(Object girlFriend, Object money) {
this.girlFriend = girlFriend;
this.money = money;
}
public void run(){
synchronized (money){
System.out.println("你放了我女朋友,我给你500万");
synchronized (girlFriend){
System.out.println("给绑匪500万");
}
}
}
}
class BangFei extends Thread{
private Object girlFriend;
private Object money;
public BangFei(Object girlFriend, Object money) {
this.girlFriend = girlFriend;
this.money = money;
}
public void run(){
synchronized (girlFriend){
System.out.println("你给我500万,我放人");
synchronized (money){
System.out.println("放人");
}
}
}
}
//尝试多次运行时会偶发死锁,是因为两个线程同时进入最外层锁后,就会等待对方释放第二个锁,导致程序无法运行下去