chap8多线程
8-1程序、进程、线程
线程:每个进程(即运行起来的程序)还可以支持并行的指向多个线程。线程拥有独立的运行栈和程序计数器PC。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KkRa5yAF-1644342913716)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206115728339.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Chxwwjei-1644342913717)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206115843369.png)]
1.内存结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lR8XwvkY-1644342913717)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206120039704.png)]
每个线程都有自己独立的栈和程序计数器
2.并行与并发
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4tc1UwLO-1644342913718)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206120413732.png)]
3.优点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KIpR7JAg-1644342913718)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206120959847.png)]
8-2 创建线程的方式
1.对Thread类的理解
java.lang.Thread 类
Thread类的对象,人如其名,可以抽象的把它看作一个线程。它要执行的内容,封装在Thread对象的run()方法中。
但直接启动这个线程的办法,是start()方法。
Thread类结构:
构造器:
Thread():创建新的Thread对象 Thread(String threadname):创建线程并指定线程实例名
Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接 口中的run方法
Thread(Runnable target, String name):创建新的Thread对象
2.方式一:Thread子类
方式一:继承Thread类
- 定义子类继承Thread类。
- 子类中重写Thread类中的run方法。
- 创建Thread子类对象,即创建了线程对象。
- 调用线程对象start方法:启动线程,调用run方法。
/**
线程的创建:方式1 Thread子类
*/
public class ThreadTest {
public static void main(String[] args) {
Mythread mythread=new Mythread();
mythread.start();
}
}
class Mythread extends Thread{
@Override
public void run() {
for (int i = 0; i <100 ; i++) {
if(i%2==0)
System.out.println(i);
}
}
}
注意:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-osiya2XU-1644342913719)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206122124519.png)]
从整个时间的角度来看,时间是这样子的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ofug9Vye-1644342913720)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20220118195011841.png)]
3.方式二:实现Runnable接口
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0goh2DDz-1644342913720)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206123645682.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8zGOIkXt-1644342913721)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206123659182.png)]
public class ThreadTest1 {
Thread thread=new Thread(new MThread());
}
class MThread implements Runnable{
@Override
public void run() {
for (int i = 0; i <100 ; i++) {
}
}
}
4.线程的有关方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-heCdXYwg-1644342913722)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206122650024.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e1Wm1YZ6-1644342913722)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206122702917.png)]
5.线程的调度
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WdyiCYhU-1644342913723)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206122742525.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gVLKVkO6-1644342913723)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206122754291.png)]
6.线程的分类:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K1aJ3RSe-1644342913724)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206122822346.png)]
8-3线程的生命周期
1.线程的几种状态
在JDK中用Thread.State类定义了线程的五种状态:
- 新建:一个Thread类的对象被创建后,start前
- 就绪:线程被start后,但没有分配到CPU资源
- 运行:当就绪的线程被调度并且获得CPU资源时
- 阻塞:在运行的过程中被临时挂起
- 死亡:被强制性的终止或者异常导致结束
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VwtQTvS9-1644342913724)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20211206134729487.png)]
8-4线程的同步
同步这一范畴的提出是基于这样的问题:
多个线程在操作共享的数据时,可能会同时进行。比如同时在银行取2000块钱,那么有可能出现只更新一次的风险。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AfQ5h091-1644342913725)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20220118221912515.png)]
下面来看一个问题来加深这个现象的理解:
模拟火车站售票程序,开启三个窗口售票。
public class WindowTest1 {
public static void main(String[] args) {
Window1 t = new Window1();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
t1.setName("t1窗口");
t2.setName("t2窗口");
t3.setName("t3窗口");
t1.start();
t2.start();
t3.start();
}
}
class Window1 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--;
}else
break;
}
}
}
如果是这样的程序,会输出票数为-1或0的情况,来分析一下为什么会出现这个问题。
在极端状态下,三个线程同时能因为是1而进入。但是都被阻塞,这样再输出时就会出现:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7IgrpiKU-1644342913725)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20220120220536191.png)]
1.多线程产生的问题
问题的原因: 当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有 执行完,另一个线程参与进来执行。导致共享数据的错误。
解决办法:
共享数据执行线程时,让一个线程先执行,其他线程不允许执行,
下面是Java当中的解决办法
2.同步的方式一:Synchronized同步代码块方式
同步机制Java中有两种写作方式:
1. 同步代码块:
synchronized (同步监视器){
// 需要被同步的代码;
}
2. synchronized还可以放在方法声明中,表示整个方法为同步方法。
例如:
public synchronized void show (String name){
….
}
1.同步的原理?其实就是多个线程,在某一过程面前停下来,只让一个先从先走,走完了其他再上。
这里,觉得的单元是逻辑代码而不是数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GXMEYpTY-1644342913725)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20220120223337636.png)]
2.同步监视器是什么?
俗称 锁
谁(某个线程)能拿到锁,就能操作后面的代码。
任何类的对象都能充当锁。
但是:多个线程,必须公用同一个锁
3.如果用同步方法来实现同步,则同步方法的锁:静态方法(类名.class)、非静态方法(this)
3.方式二:同步方法
代码示例:
/**
* 同步方法来解决线程同步问题
*
*/
public class WindowTest2 {
public static void main(String[] args) {
Window2 t = new Window2();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
t1.setName("t1窗口");
t2.setName("t2窗口");
t3.setName("t3窗口");
t1.start();
t2.start();
t3.start();
}
}
class Window2 implements Runnable{
private int ticket=100;
Object obj=new Object();
@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--;
}
}
}
1.同步方法好像没有指出同步监视器,它真的没有锁吗?
答案是有,就是this,即本对象。
4.对同步问题的讨论
1、如何找问题,即代码是否存在线程安全?(非常重要)
(1)明确哪些代码是多线程运行的代码
(2)明确多个线程是否有共享数据
(3)明确多线程运行代码中是否有多条语句操作共享数据
2、如何解决呢?
(非常重要) 对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。 即所有操作共享数据的这些语句都要放在同步范围中
3、切记:
范围太小:没锁住所有有安全问题的代码
范围太大:没发挥多线程的功能。
5.释放锁
在遇到下面的情况时,线程会把锁释放掉,以给下一个线程占据
当前线程的同步方法、同步代码块执行结束。
当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、 该方法的继续执行。
当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导 致异常结束。
当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线 程暂停,并释放锁。
不会释放锁的操作:
注意:线程执行同步代码块或同步方法时,程序调用Thread.sleep()、 Thread.yield()方法暂停当前线程的执行 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程 挂起,该线程不会释放锁(同步监视器)。 应尽量避免使用suspend()和resume()来控制线程
6.单例设计模式之懒汉式(线程安全)
所谓的懒汉式,就是这样设计的类:
提供一个公共静态方法来获取类的对象
public class Bank {
private Bank(){}
private static Bank instance=null;
public static Bank getInstance(){
if (instance==null){
instance=new Bank();
}
return instance;
}
}
- 因为有判定语句,所以在多线程运行时,可能会创建两个对象,线程不安全。因此要对这个设计模式进行线程安全改造
public class Bank {
private Bank(){}
private static Bank instance=null;
public synchronized static Bank getInstance(){
if (instance==null){
instance=new Bank();
}
return instance;
}
}
7.死锁问题
定义:
不同的线程,分别占有对方线程所需要的资源不放弃,这样,双方都无法获得资源而结束自己的线程。
演示死锁问题:
package com.atguigu.java1;
//死锁的演示
class A {
public synchronized void foo(B b) { //同步监视器:A类的对象:a
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了A实例的foo方法"); // ①
// try {
// Thread.sleep(200);
// } catch (InterruptedException ex) {
// ex.printStackTrace();
// }
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用B实例的last方法"); // ③
b.last();
}
public synchronized void last() {//同步监视器:A类的对象:a
System.out.println("进入了A类的last方法内部");
}
}
class B {
public synchronized void bar(A a) {//同步监视器:b
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了B实例的bar方法"); // ②
// try {
// Thread.sleep(200);
// } catch (InterruptedException ex) {
// ex.printStackTrace();
// }
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用A实例的last方法"); // ④
a.last();
}
public synchronized void last() {//同步监视器:b
System.out.println("进入了B类的last方法内部");
}
}
public class DeadLock implements Runnable {
A a = new A();
B b = new B();
public void init() {
Thread.currentThread().setName("主线程");
// 调用a对象的foo方法
a.foo(b);
System.out.println("进入了主线程之后");
}
public void run() {
Thread.currentThread().setName("副线程");
// 调用b对象的bar方法
b.bar(a);
System.out.println("进入了副线程之后");
}
public static void main(String[] args) {
DeadLock dl = new DeadLock();
new Thread(dl).start();
dl.init();
}
}
8.方式三:Lock锁机制来实现同步
lock机制是java5的新特性,通过显式定义同步锁对象来实现同步。
不过Lock只是个接口,ReentrantLock才是实现类。用它来实现代码的同步:
class Window implements Runnable{
private int ticket = 100;
//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try{
//2.调用锁定方法lock()
lock.lock();
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
ticket--;
}else{
break;
}
}finally {
//3.调用解锁方法:unlock()
lock.unlock();
}
}
}
}
/**
* 解决线程安全问题的方式三:Lock锁 --- JDK5.0新增
*
* 1. 面试题:synchronized 与 Lock的异同?
* 相同:二者都可以解决线程安全问题
* 不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
* Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())
*
* 2.优先使用顺序:
* Lock 同步代码块(已经进入了方法体,分配了相应资源) 同步方法(在方法体之外)
*
*
* 面试题:如何解决线程安全问题?有几种方式
* @author shkstart
* @create 2019-02-15 下午 3:38
*/
8-5线程的通信
解释:线程的通信就是两个线程之间,建立一种关系。
例如,下面这个例题:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TL7nbDTh-1644342913726)(C:\Users\86198\AppData\Roaming\Typora\typora-user-images\image-20220208225749574.png)]
package com.atguigu.java2;
/**
* 线程通信的例子:使用两个线程打印 1-100。线程1, 线程2 交替打印
*
*
* 涉及到的三个方法:
* wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
* notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
* notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
*
* 说明:
* 1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
* 2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
* 否则,会出现IllegalMonitorStateException异常
* 3.wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
*
* 面试题:sleep() 和 wait()的异同?
* 1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
* 2.不同点:1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
* 2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
* 3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
*
* @author shkstart
* @create 2019-02-15 下午 4:21
*/
class Number implements Runnable{
private int number = 1;
private Object obj = new Object();
@Override
public void run() {
while(true){
synchronized (obj) {
obj.notify();
if(number <= 100){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
//使得调用如下wait()方法的线程进入阻塞状态
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
- 面试题:sleep() 和 wait()的异同?
- 1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
- 2.不同点:1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
- 2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
- 3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
例题1 生产者问题
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处 取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图 生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通 知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如 果店中有产品了再通知消费者来取走产品。 这里可能出现两个问题: 生产者比消费者快时,消费者会漏掉一些数据没有取到。 消费者比生产者快时,消费者会取相同的数据。
解答:关键在于,把Clerk作为中间类,消费者和生产资作为进程类。
每当产品大于20个时,终止当前线程(这时,线程肯定是生产者线程)
小于0个时,终止当前线程。
class Clerk{
private int productCount = 0;
//生产产品
public synchronized void produceProduct() {
if(productCount < 20){
productCount++;
System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品");
notify();
}else{
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消费产品
public synchronized void consumeProduct() {
if(productCount > 0){
System.out.println(Thread.currentThread().getName() + ":开始消费第" + productCount + "个产品");
productCount--;
notify();
}else{
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer extends Thread{//生产者
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":开始生产产品.....");
while(true){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produceProduct();
}
}
}
class Consumer extends Thread{//消费者
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName() + ":开始消费产品.....");
while(true){
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumeProduct();
}
}
}
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk);
p1.setName("生产者1");
Consumer c1 = new Consumer(clerk);
c1.setName("消费者1");
Consumer c2 = new Consumer(clerk);
c2.setName("消费者2");
p1.start();
c1.start();
c2.start();
}
}