目录
Object类的wait(),notify(),notifyAll()
进程与线程
-
进程是指运行中的应用程序,每一个进程都有自己独立的内存空间,是一个实体的存在
-
线程是指进程中的一个执行流程,有时也称为执行情景
-
进程可以有多个线程组成,即使在同一个进程可以同时运行多个不同的线程,分别执行不同的任务
-
好比如一辆火车-火车就是一个进程,线程则是由多个车厢组成
-
-
当进程内的多个线程同时运行,这种运行方式称为并发
-
单线程:同一时刻,只允许执行一个线程
-
多线程:同一时刻,可以执行多个线程,比如:一个qq进程,可以打开多个聊天窗口。一个百度网盘可以同时下载多个文件
相关概念:
-
并发:同个时刻,多个任务交替进行,造成一种同时的错觉
-
单核CPU实现的任务就是并发
-
-
并行:同个时刻,多个任务同时进行
-
多核CPU可以实现并行
-
运行机制
每个线程都有一个独立的程序计数器和方法调用栈(method invocation stack):程序计数器:也称为PC寄存器,当线程执行一个方法时,程序计数器指向方法区中下一条要执行的字节码指令。
方法调用栈:简称方法栈,用来跟踪线程运行中一系列的方法调用过程,栈中的元素称为栈桢。每当线程调用一个方法,就会向方法栈压入一个新桢,桢用来存储方法的参数、局部变量和运算过程中的临时数据
-
方法调用栈:简称方法栈,用来跟踪线程运行中一系列的方法调用过程,栈中的元素称为栈帧,每当线程调用一个方法,就会向方法压入一个新帧,帧用来存储方法的参数,局部变量和运算过程中的临时数据
-
栈区先是为空,当调用main方法时,便将main方法压入栈,称为栈帧,后调用method方法,就将method方法加入栈
线程的创建和启动
创建线程有两种方式
-
继承Thread类
-
实现Runnable接口
继承Thread
-
Thread类代表线程类,它的最主要的两个方法
-
run()包含线程运行时所执行的代码
-
start()用于启动线程
-
特性
-
每个线程都是通过某个特定的Thread对象的run()方法来完成操作的,经常吧run()方法的主体称为线程体
-
通过Thread方法的start()方法来启动这个线程,而非直接调用run()
-
-
创建一个继承于Thread类的子类
-
重写Thread类的run()方法
-
创建Thread类的子类的对象
-
通过此对象调用start()来启动一个线程
public class Machine extends Thread{
@Override
public void run(){
for(int i=0; i < 50; i++){
System.out.println(i);
}
}
public static void main(String args[]){
Machine machine = new Machine();
machine.start(); //启动machine线程
}
}
例题
打印一百以内的偶数,一百以内的基数
public class ThreadTest {
public static void main(String[] args) {
MyThread1 m1 = new MyThread1();
MyThread2 m2 = new MyThread2();
m1.start();
m2.start();
}
}
class MyThread1 extends Thread{
@Override
public void run(){
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() +":"+ i);
}
}
}
}
class MyThread2 extends Thread{
@Override
public void run(){
for (int i = 0; i < 100; i++) {
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() +":"+ i);
}
}
}
}
实现Runnable
-
创建一个实现Runnable接口的类
-
实现类去实现Runnable接口中的抽象方法:run()
-
创建实现类的对象
-
将此对象作为参数传到Thread类的构造器中,创建Thread类的对象
-
通过Thread类的对象调用start()方法
public class Machine implements Runnable{
@Override
public void run(){
for(int i = 0; i < 50; i++){
System.out.println(Thread.currentThread().getName() +":"+ i);
try{
Thread.sleep(100);
}catch(InterruptedException e){
throw new RuntimeException(e);
}
}
}
public static void main(String args[]){
Machine machine = new Machine();
Thread t1 = new Thread(machine);
Thread t2 = new Thread(machine);
t1.start();
t2.start();
}
}
Thread和Runnable区别
-
通过继承Thread或者实现Runnable接口来创建线程本质上没有区别
-
实现Runnable接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制
-
相同点两种方式都需要重写run()方法,线程的执行逻辑都在run()方法中
列题
窗口卖票 -----
public class ThreadPracticeTest {
public static void main(String[] args) {
Window t1 = new Window();
Window t2 = new Window();
Window t3 = new Window();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window extends Thread {
private static int ticket = 100;
@Override
public void run() {
while(true){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() +"---"+ ticket);
ticket --;
}else{
break;
}
}
}
}
更换Runnable
public class ThreadPracticeTest {
public static void main(String[] args) {
Window w1 = new Window();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window implements Runnable {
private int ticket = 100;
@Override
public void run() {
while(true){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() +"---"+ ticket);
ticket --;
}else{
break;
}
}
}
}
线程生命周期
-
新建状态(New):new语句创建的线程对象处于新建状态,此时它和其它对象一样,仅仅在堆区中被分配了内存
-
就绪状态(Runnable):当一个线程对象创建后,其它线程调用它的start()方法。该线程就进入就绪状态,Java虚拟机会为它创建方法调用栈,,等待CPU的使用
-
运行状态(Running):运行状态的线程占用CPU执行程序代码,在并发运行环境中,如果计算机只有一个CPU,那么任何是可只会有一个线程处于这个状态,如果计算机有多个CPU,那么同一时刻可以让几个线程占用不同的CPU,使它们都处于运行状态
-
阻塞状态(Blocked):指线程因为某些原因放弃CPU,暂时停止运行。当线程处于阻塞状态时,Java虚拟机不会给线程分配CPU,直到线程重新进入就绪状态,它才有机会转到运行状态
-
分为三种
-
位于对象等待池中的阻塞状态(Blocked in object’s wait pool):当线程处于运行状态,如果执行了某个对象的wait()方法,Java虚拟机就会把线程放到这个对象的等待池中
-
位于对象锁池中的阻塞状态(Blocked in object’s lock pool):当线程处于运行状态,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其它线程占用,Java虚拟机就会把这个线程放到这个对象的锁池中。
-
其他阻塞状态(Otherwise Blocked):当前线程执行了sleep()方法,或者调用了其他线程的join()方法,或者发出了I/O请求,就会进入这个状态
-
-
-
死亡状态(Dead): 当线程退出run()方法,就进入死亡状态,该线程结束生命周期。线程有可能是正常执行完run()方法而退出,也有可能是遇到异常而退出
线程常用方法
-
start() 启动当前线程,调用当前线程的run()
-
run() 通过需要重写Thread类中的此方法,将创建线程要执行的操作声明在此方法中
-
currentThread() 静态方法,返回当前代码执行的线程
-
getName() 获取当前线程的名字
-
setName() 设置当前线程的名字
-
yield() 释放CPU执行权
-
join() 在线程a中调用线程b的join(),此时线程a进入阻塞状态,直到线程b执行完以后,线程a才结束阻塞状态
-
stop() 已过时,当执行此方法时,强制结束当前线程
-
sleep(long militime) 让线程睡眠指定的毫秒数,在指定时间内,线程时阻塞状态
-
isAlive() 判断当前线程是否存活
-
setPriority() 更改线程的优先级
-
getPriority() 获取线程的优先级
public class ThreadMethodTest {
public static void main(String[] args) {
Thread1 t1 = new Thread1();
t1.start();
Thread.currentThread().setName("主线程"); //设置线程名
for (int i = 0; i < 100; i++) {
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() +":"+ i);
}
if(i == 20){
// Thread.yield();
// System.out.println("释放CPU"+ i);
// try {
// System.out.println("阻塞");
// Thread.currentThread().join();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}
Thread.currentThread().isAlive();
}
}
class Thread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
try {
sleep(1000);
System.out.println(Thread.currentThread().getName() +":"+ i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
线程的调度
-
同优先级线程组成先进去先出列(先到先服务),使用时间片策略
-
对高优先级,使用优化制度的抢占式策略
线程优先级等级
-
MAX_PRIORITY:10
-
MIN_PRIORITY:1
-
NORM_PRIORITY:5
涉及的方法
-
getPriority() 返回线程优先值
-
setPriority(int newPriority) 改变线程优先级
说明:
-
线程创建时继承父线程的优先级
-
低优先级只是获得调度的概率低,反之只是概率高了,并不是百分百优先,百分百低优先
public class ThreadPriorityTest {
public static void main(String[] args) {
ThreadOne t1 = new ThreadOne();
t1.setName("子线程");//设置线程名称
t1.setPriority(Thread.MAX_PRIORITY);//设置线程调度
t1.start();
Thread.currentThread().setName("主线程");
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() +"-"+ Thread.currentThread().getPriority()+"--"+ i);
}
}
}
class ThreadOne extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() +"-"+ getPriority() +"--"+ i);
}
}
}
线程同步
多线程的安全性问题
-
多个线程执行的不确定性硬气执行结果的不稳定性
-
多个线程对账本的共享,会造成操作的不完整性,会破坏数据
-
多个线程访问共享的数据时可能存在安全性问题
线程的安全问题Demo: 卖票过程中出现了重票和错票的情况 (以下多窗口售票demo存在多线程安全问题)
public class ThreadPracticeTest {
public static void main(String[] args) {
Window w1 = new Window();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window implements Runnable {
private int ticket = 100;
@Override
public void run() {
while(true){
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +"---"+ ticket);
ticket --;
}else{
break;
}
}
}
}
错误:当票数为1的时候,三个线程中有线程被阻塞没有执行票数-1的操作,这是其它线程就会通过if语句的判断,这样一来就会造成多卖了一张票,出现错票的情况。
极端情况为,当票数为1时,三个线程同时判断通过,进入阻塞,然后多执行两侧卖票操作
解决
当线程a在操作ticket的时候,其它的线程不能够进入操作,必须等线程a执行完成ticket时,其它的线程才能够开始操作ticket,即使出现了线程阻塞状态也不可
在Java中通过同步机制,解决线程安全问题
-
同步代码块
-
synchronized(同步监视器){}
-
操作共享数据的时候需要被同步操作,即为多个线程共同操作的称为共享数据
-
同步监视器,俗称:锁。任何一个类的对象,都可以充当锁
-
public class ThreadPracticeTest {
public static void main(String[] args) {
Window w1 = new Window();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window implements Runnable {
private int ticket = 100;
@Override
public void run() {
while(true){
synchronized(this) {//添加同步锁-同步代码块
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
同步的代码不能包含太多,不能少,需要共享的数据代码包含即可
-
同步方法
-
synchronized(){}
-
public class ThreadPracticeTest {
public static void main(String[] args) {
Window w1 = new Window();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window implements Runnable {
private int ticket = 100;
@Override
public void run() {
while(true){
show();
}
}
private synchronized void show(){
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---" + ticket);
ticket--;
}else{//不加则会进入死循环
System.exit(1);
}
}
}
多个线程共用同一个锁,只能有一个对象
-
如果是使用Thread类继承方式实现
-
则需要在对应的位置上加上 static静态,保存对象是唯一的状态即可
说明:使用同步的好处解决了线程的安全问题,操作同步代码时,只能有一个线程参与执行,其它线程等待执行,相当于单线程执行,效率低,线程安全问题就得这样执行,牺牲效率提高安全
单例模式
解决懒汉式线程安全
-
使用同步机制将单例模式的懒汉式改为线程安全
public class SingletonTest {
public static void main(String[] args) {
Bank b1 = Bank.getInstance();
Bank b2 = Bank.getInstance();
System.out.println(b1 == b2);
}
}
class Bank{
private Bank(){}
private static Bank instance = null;
public static Bank getInstance(){
//方式一、效果较低
// synchronized (Bank.class) {
// if (instance == null) {
// instance = new Bank();
// }
// }
//方式二、双重校验提升效率
if(instance == null){
synchronized (Bank.class) {
if(instance == null) {
instance = new Bank();
}
}
}
return instance;
}
}
线程死锁
死锁就是指两个线程在执行过程中,竞争资源或者彼此通信而造成的一种阻塞线程,若无外力作用,无法推进下去,导致成死锁现象
张三和李四一起去饺子馆吃饺子. 吃饺子需要酱油和醋 张三拿起了酱油瓶, 李四拿起了醋瓶 张三: 你先把醋瓶给我, 我用完了就把酱油瓶给你 李四: 你先把酱油瓶给我, 我用完了就把醋瓶给你 如果这俩人彼此之间互不相让, 就构成了死锁 酱油和醋相当于是两把锁, 这两个人就是两个线程
解决
-
预防死锁:通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个条件,来防止死锁的发生
-
避免死锁:在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免死锁的发生
-
检测死锁:允许系统在运行过程中发生死锁,但可设置检测机构及时检测死锁的发生,并采取适当措施加以清除
-
解除死锁:当检测出死锁后,便采取适当措施将进程从死锁状态中解脱出来
死锁演示
public class DeadLockRunnable implements Runnable {
private int flag; //决定线程走向的标记
//需要注意这两个对象一定要是两个实现共享
//不然每次new的就会是这两个对象
private static Object o1 = new Object();
private static Object o2 = new Object();
public DeadLockRunnable(int flag){
this.flag = flag;
}
@Override
public void run() {
if(flag == 1){
//线程1执行代码
synchronized (o1){
System.out.println(Thread.currentThread().getName() +"获取到资源o1,请求o2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
System.out.println(Thread.currentThread().getName() +"获取到资源o1,和o2");
}
}
}else{
//线程1执行代码
synchronized (o2){
System.out.println(Thread.currentThread().getName() +"获取到资源o2,请求o1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
System.out.println(Thread.currentThread().getName() +"获取到资源o2,和o1");
}
}
}
}
}
class Test{
public static void main(String[] args) {
//创建两个对象实例
DeadLockRunnable d1 = new DeadLockRunnable(1);
DeadLockRunnable d2 = new DeadLockRunnable(2);
//创建两个线程执行两个对象实例
Thread t1 = new Thread(d1,"DeadLock1");
Thread t2 = new Thread(d2,"DeadLock2");
//启动线程
t1.start();
t2.start();
}
}
形成死锁条件
-
互斥条件:线程(进程)对于所分配到的资源具有排他性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放
-
请求与保持条件:一个线程(进程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放
-
不剥夺条件:线程(进程)已获得的资源在未使用完之前不能被其它线程强行剥夺,只有自己使用完毕后才释放资源
-
循环等待条件:当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似死循环),造成永久阻塞
解决方式
-
只要破环产生死锁的四个条件中的其中之一就可以了
-
互斥条件
-
这个条件没有办法破坏,因为用锁本来就是想让他们互斥
-
-
请求与保持条件
-
一次性申请所有的资源
-
-
不剥夺条件
-
占用部分资源的线程进一步申请其它资源时,如果申请不到,可以主动释放它占有的资源
-
-
循环等待条件
-
靠按序申请资源来预防,按某一顺序申请资源,释放资源则反序释放,破坏循环等待条件
-
-
两个线程以不同的顺序来获得相同的锁,如果按照相同顺序来请求锁,那么就不会出现循环的枷锁依赖性,因此也就不会产生死锁,每个需要锁o1和锁o2的线程都以相同的顺序来获取o1和o2,那么就不会发生死锁
死锁
无死锁
public class DeadLockRunnable implements Runnable {
private int flag; //决定线程走向的标记
//需要注意这两个对象一定要是两个实现共享
//不然每次new的就会是这两个对象
private static Object o1 = new Object();
private static Object o2 = new Object();
public DeadLockRunnable(int flag){
this.flag = flag;
}
@Override
public void run() {
if(flag == 1){
//线程1执行代码
synchronized (o1){
System.out.println(Thread.currentThread().getName() +"获取到资源o1,请求o2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (o2){
System.out.println(Thread.currentThread().getName() +"获取到资源o1,和o2");
}
}else{
//线程1执行代码
synchronized (o2){
System.out.println(Thread.currentThread().getName() +"获取到资源o2,请求o1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (o1){
System.out.println(Thread.currentThread().getName() +"获取到资源o2,和o1");
}
}
}
}
class Test{
public static void main(String[] args) {
//创建两个对象实例
DeadLockRunnable d1 = new DeadLockRunnable(1);
DeadLockRunnable d2 = new DeadLockRunnable(2);
//创建两个线程执行两个对象实例
Thread t1 = new Thread(d1,"DeadLock1");
Thread t2 = new Thread(d2,"DeadLock2");
//启动线程
t1.start();
t2.start();
}
}
锁Lock
从 JDK 1.5 开始,Java 提供了更强大的线程同步机制:通过显示定义同步锁对象来实现同步。同步锁使用 Lock 对象充当。
接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象
Lock和synchronized比较
-
Lock 显示锁,手动加锁、解锁;用 Lock,JVM 将花费较少的时间来调度线程,性能更好
-
synchronized 隐式锁,出了作用域自动释放
public class DeadLock implements Runnable {
private int ticket = 10;
// 可重入锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock(); //手动锁
try {
if (ticket <= 0) {
break;
}
try {
Thread.sleep(1000);
System.out.println(ticket--);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();//释放锁
}
}
}
}
class DeadLockTest {
public static void main(String[] args) {
DeadLock d1 = new DeadLock();
Thread t1 = new Thread(d1);
Thread t2 = new Thread(d1);
Thread t3 = new Thread(d1);
t1.start();
t2.start();
t3.start();
}
}
Object类的wait(),notify(),notifyAll()方法
方法名 | 作用 |
---|---|
void wait() | 让活动在当前对象的线程无限等待(释放之前占有的锁) |
void notify() | 唤醒当前对象正在等待的线程(只提示唤醒,不会释放锁) |
void notifyAll() | 唤醒当前对象全部正在等待的线程(只提示唤醒,不会释放锁) |
-
wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器(锁)
-
notify():一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程被wait,就唤醒优先级高的
-
notifyAll():一旦执行此方法,就会唤醒所有被wait的线程
说明:以上三个方法必须使用在同步代码块或同步方法中,三个方法的调用者必须是同步代码块或同步方法中的同步监视器!否则出现 IllegalMonitorStateException 异常
public class Number implements Runnable {
private int number = 1;
//private Object obj = new Object();
@Override
public void run() {
while (true){
// synchronized (obj){
synchronized (this){
// obj.notify();
this.notify();
if(number <= 10) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "----" + number);
number ++;
try {
// obj.wait();//阻塞状态,并释放锁
this.wait();//阻塞状态,并释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
class NumberTest {
public static void main(String[] args) {
Number n = new Number();
Thread t1 = new Thread(n);
Thread t2 = new Thread(n);
t1.start();
t2.start();
}
}