多线程
一:程序、进程、线程的基本概念
1.程序(program):就是为了完成特定任务,用某种编程语言编写的一组指令(代码)的集合。即指一段静态的代码。
例如:IDEA程序,一堆文件夹就可以看作是静态的指令
2.进程(process):是程序的一次执行过程,或是正在运行的一个程序。它是一个动态的过程:有着自身的产生、存在和消亡的过程——生命周期。
例如:运行中的PotPlayer、IDEA……
3.线程(thread):是一个程序内部的一条执行路径。
多线程——若一个进程同一时间并行执行多个线程,就是支持多线程的。
一个Java应用程序java.exe,至少有三个线程:main()主线程、gc()垃圾回收线程、异常处理线程。
例如:电脑管家、360安全卫士可以并行进行电脑检测,垃圾清理,病毒查杀。
4.进程与线程的细节:
①、进程作为资源分配单位,系统在运行时会为每个进程分配不同的内存区域。JVM会为每一个进程分配堆和本地方法区。
②、线程作为调度和执行单位,每个线程独立拥有一套虚拟机栈和程序计数器,线程切换的开销小。
③、一个进程中的多个线程共享相同的内存单元(内存地址空间),那么它么从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程之间的通信更简便、高效。但是:多个线程操作共享的系统资源可能会带来安全问题。(线程安全为题)
二:线程的创建和使用(重点)
1.继承Thread类的方式创建线程
package cn.xuguowen.java;
/**
* @author xuguowen
* @create 2021-02-17 19:44
* @Description 多线程的创建方式一:继承Thread类
* 1.继承Thread类
* 2.重写run()方法 ---> 将此线程执行的操作声明在run()方法当中
* 3.创建Thread类的子类对象
* 4.通过对象调用父类start()方法
*
* 例如:求100以内的偶数
*/
public class ThreadTest {
public static void main(String[] args) {
// 3.创建Thread类的子类对象
MyThread myThread = new MyThread();
// 4.调用父类start()方法:①启动当前线程、②调用当前线程的run()方法
myThread.start();
// 以下代码仍然在main线程中执行的
for (int i = 0; i < 101; i++) {
if (i % 2 == 0) {
System.out.println(i + "*************main()***************");
}
}
}
}
// 1.继承Thread类
class MyThread extends Thread{
// 2.重写run()方法
@Override
public void run() {
// 求100以内的偶数
for (int i = 0; i < 101; i++) {
if (i % 2 == 0) {
System.out.println(i);
}
}
}
}
运行结果:
start()方法的说明:①开始当前线程 ② JVM调用当前线程的run()方法
- 问题一:为何不直接调用run()方法呢?
如果直接调用run()方法的话,就不存在多线程了。只存在主线程按照顺序依次执行代码。为了证明不存在多线程了,我们使用Thread类的API证明。
package cn.xuguowen.java;
/**
* @author xuguowen
* @create 2021-02-17 19:44
* @Description 多线程的创建方式一:继承Thread类
* 1.继承Thread类
* 2.重写run()方法 ---> 将此线程执行的操作声明在run()方法当中
* 3.创建Thread类的子类对象
* 4.通过对象调用父类start()方法
*
* 例如:求100以内的偶数
*/
public class ThreadTest {
public static void main(String[] args) {
// 3.创建Thread类的子类对象
MyThread myThread = new MyThread();
// 4.调用父类start()方法 ①启动当前线程 ②调用当前线程的run()方法
// myThread.start();
myThread.run();
// 以下代码仍然是在main线程中执行的
for (int i = 0; i < 101; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i + "*************main()***************");
}
}
}
}
// 1.继承Thread类
class MyThread extends Thread{
// 2.重写run()方法
@Override
public void run() {
// 求100以内的偶数
for (int i = 0; i < 101; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
证明发现:执行完run()方法,才会执行下面的代码。说明并没有启动该线程,调用start()方法。
- 问题二:如何再次启动一个线程?
创建Thread类的另一个字类对象,调用start()方法。
package cn.xuguowen.java;
/**
* @author xuguowen
* @create 2021-02-17 19:44
* @Description 多线程的创建方式一:继承Thread类
* 1.继承Thread类
* 2.重写run()方法 ---> 将此线程执行的操作声明在run()方法当中
* 3.创建Thread类的子类对象
* 4.通过对象调用父类start()方法
*
* 例如:求100以内的偶数
*/
public class ThreadTest {
public static void main(String[] args) {
// 3.创建Thread类的子类对象
MyThread myThread0 = new MyThread();
// 4.调用父类start()方法 ①启动当前线程 ②调用当前线程的run()方法
myThread0.start();
// myThread.run();
// 再次创建一个线程对象
MyThread myThread1 = new MyThread();
myThread1.start();
// 以下代码仍然是在main线程中执行的
for (int i = 0; i < 101; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i + "*************main()***************");
}
}
}
}
// 1.继承Thread类
class MyThread extends Thread{
// 2.重写run()方法
@Override
public void run() {
// 求100以内的偶数
for (int i = 0; i < 101; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
why—>为什么一个线程对象不能调用两次start()方法呢?
通关观察源码发现:Thread类中的start()方法会改变当前线程的状态,即调用start()方法之后,线程的状态为0;如果再次调用start()方法,状态不再是0,则会抛出线程状态异常(IllegalThreadStateException)。
2.实现Runnable接口的方式创建线程
package cn.xuguowen.java;
/**
* @author xuguowen
* @create 2021-02-18 14:25
* @Description 创建多线程的方式二: 实现Runnable接口
* 1.创建一个Runnable接口的实现类
* 2.实现接口中的抽象方法,并完成特定的操作
* 3.创建实现类对象
* 4.将实现类对象作为参数传递到线程对象中
* 5.通过线程对象调用start()方法
*/
public class ThreadTest1 {
public static void main(String[] args) {
// 3.创建实现类对象
RunnableImpl r1 = new RunnableImpl();
// 4.将实现类对象作为参数传递到线程对象中
Thread t1 = new Thread(r1,"线程1");
// 5.通过线程对象调用start()方法:①启动当前线程 ②调用当前线程的run()方法
// 问题一:为什么是调用了实现类中的run()方法--->看源码
t1.start();
// 再创建一个线程
Thread t2 = new Thread(r1,"线程2");
t2.start();
}
}
// 1.创建一个Runnable接口的实现类
class RunnableImpl implements Runnable{
// 2.实现接口中的抽象方法,并完成特定的操作
@Override
public void run() {
for (int i = 0; i < 101; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
- 问题一:为什么是调用了实现类中的run()方法?—>看源码发现
3.比较创建线程的两种方式
例题:实现卖票的功能。总票数100张,创建3个窗口卖票。(3个线程)
存在线程安全为题,待解决。仅仅是用来体现开发中常用实现Runnable接口的方式创建线程。
①:使用继承Thread类的方式实现例题
package cn.xuguowen.java;
/**
* @author xuguowen
* @create 2021-02-18 14:00
* @Description 创建三个窗口买票,总票数为100张.使用继承Thread类的方式实现
* 存在线程安全问题,待解决。
*
*/
public class SellTickets {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
// 运行之后发现 3 个线程对象独立拥有ticket属性,相当于300张票,这是错误的。
// 解决方式:将ticket修改为静态成员变量,3 个线程对象共享
// 如果不把成员变量变为静态的,该如何解决这个问题呢?
// 改为静态成员变量之后发现第100张票还是卖了3次,这就涉及到线程的安全问题。待解决。
}
}
class Window extends Thread{
private static int ticket = 100; // 总票数
// 实现run()方法
@Override
public void run() {
while (true) {
if (ticket > 0) {
// 进行卖票
System.out.println(getName() + ": 卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
②:使用实现Runnable接口的方式完成例题
package cn.xuguowen.java;
/**
* @author xuguowen
* @create 2021-02-18 14:57
* @Description 创建三个窗口买票,总票数为100张.使用实现Runnable接口的方式
* 存在线程安全问题,待解决。
*/
public class SellTickets1 {
public static void main(String[] args) {
// 创建实现类对象
Window1 w1 = new Window1();
// 创建线程对象
Thread t1 = new Thread(w1, "窗口1");
Thread t2 = new Thread(w1, "窗口2");
Thread t3 = new Thread(w1, "窗口3");
// 启动窗口,卖票
t1.start();
t2.start();
t3.start();
// 此时,还会存在第100张票重复,出现线程安全问题。待解决
// 但是,使用实现Runnable接口的方式创建多线程,可以共用同一个对象,也就可以共用同一个成员变量。
// 使用继承Thread的方式,字类必须将实例变量声明为静态变量,此时才可共用这个变量。否则是每个对象所单独拥有的一份
}
}
class Window1 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;
}
}
}
}
结论: 在实际开发中,优先选择实现Runnable接口的方式创建线程。
①.打破了java中的单继承性;
②.实现Runnable接口的方式可以处理多个线程之间共享数据的情况;
③.Thread类也实现了Runnable接口。
4.callable接口的方式创建线程(JDK5.0新增)
package cn.xuguowen.java4;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @author xuguowen
* @create 2021-02-21 14:05
* @Description 使用实现Callable接口的方式创建多线程
*
* 如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建的多线程强大?
* 1.call()方法有返回值,可以作为其他线程的资源
* 2.可以抛出异常,外面可以捕获异常,获取异常信息
* 3.Callable接口是支持泛型的
*/
public class CallableTest {
public static void main(String[] args) {
// 3.创建实现类对象
NewThread newThread = new NewThread();
// 创建FutureTask对象,将实现类对象作为参数传入进去
FutureTask<Integer> f = new FutureTask<>(newThread);
// 创建线程类,将FutureTask对象作为Runnale接口的实现类对象传递进去
// FutureTask<V> implements RunnableFuture<V>
// RunnableFuture<V> extends Runnable, Future<V> {
Thread t = new Thread(f);
t.start();
// 当我们对call方法的返回值感兴趣时,可以调用FutureTask类中的get方法获取call方法的返回值
try {
Integer sum = f.get();
System.out.println("总和:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
// 1.创建实现类对象
class NewThread implements Callable<Integer> {
// 2.实现call方法
@Override
public Integer call() throws Exception {
int sum = 0;
// 求100以内偶数之和
for (int i = 1; i < 101; i++) {
if (i % 2 == 0) {
System.out.println(i);
sum += i;
}
}
// 自动装箱
return sum;
}
}
5.线程池
5.1背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
5.1好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
package cn.xuguowen.java4;
import java.util.concurrent.*;
/**
* @author xuguowen
* @create 2021-02-21 14:46
* @Description 使用线程池的方式创建多线程
*
* 线程池的好处?
* 1.提高了响应速度,减少了创建新线程花费的时间
* 2.降低了资源消耗,重复利用线程池中的线程,不需要每次都创建线程
* 3.便于对线程的管理
* corePoolSize:核心池的大小
* MaximumPoolSize:最大线程数
* keepAliveTime:线程没有任务时最多保持多长时间后会终止
* …………
*/
public class ThreadPoolTest {
public static void main(String[] args) {
// 1.使用线程池的工具类创建线程池对象,线程池中有10个线程对象
// ExecutorService是一个接口
ExecutorService service = Executors.newFixedThreadPool(10);
// 如何设置线程的属性呢?由于上面是一个接口,接口中全是全局常量,所以不会存在线程的属性的
// 2.通过getClass方法获取接口的实现类对象
System.out.println(service.getClass()); //class java.util.concurrent.ThreadPoolExecutor
ThreadPoolExecutor s = (ThreadPoolExecutor) service;
s.setMaximumPoolSize(2);
// 3.执行指定的线程操作
service.execute(new ThreadPool()); // 适用于Runnable接口3
ThreadPool2 t = new ThreadPool2();
FutureTask futureTask = new FutureTask(t);
service.submit(futureTask); // 适用于Callable接口
// 4.记住关闭线程池
service.shutdown();
}
}
class ThreadPool implements Runnable{
@Override
public void run() {
for (int i = 0; i < 101; i++) {
if (i % 2 == 0) {
System.out.println(i);
}
}
}
}
class ThreadPool2 implements Callable{
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i < 101; i++) {
if (i % 2 != 0) {
System.out.println(i);
}
}
return sum;
}
}
三:线程的常用方法和优先级
package cn.xuguowen.java1;
/**
* @author xuguowen
* @create 2021-02-18 10:40
* @Description 测试Thread类中常用的方法
*
* 1.start():开启当前线程,并且JVM执行当前线程的run()方法
* 2.run():通常重写Thread类中的run()方法,将创建的线程要执行的操作声明在次方法中
* 3.currentThread():静态方法,获取执行当前代码的线程对象
* 4.setName():设置当前线程的名字
* 也可以通过构造器的方式为线程命名:Thread(String name)
* 5.getName():获取当前线程的名字
* 6.yield():释放当前cpu的执行权。
* 7.join():在线程a中调用线程b的join()方法,此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束
* 阻塞状态。
* 8.stop():已过时。当执行此方法时,强制结束当前线程
* 9.sleep(long millitime):让当前线程‘睡眠’指定的毫秒数。在指定的毫秒数之内,当前线程是阻塞状态的。
* 10.isAlive():判断当前线程是否存活。
*
*
* 线程优先级的问题;
* 1.首先Thread类提供了3个常量来设置线程的优先级
* public final static int MIN_PRIORITY = 1;
* public final static int NORM_PRIORITY = 5; 默认优先级
* public final static int MAX_PRIORITY = 10;
* 2.通过方法设置和获取线程的优先级值
* getPriority():获取线程的优先级,默认情况下,线程的优先级都是 5
* setPriority():设置线程的优先级
*
* 说明:高优先级的线程要抢占低优先级线程的cpu执行权。这只是从概率上讲,高优先级的线程高概率情况下被执行。
* 并不意味着只有高优先级的线程执行完毕之后,低优先级的线程才会执行。
*/
public class ThreadMethodTest {
public static void main(String[] args) {
// 创建线程对象,通过构器的方式为线程命名
SubThread st = new SubThread("Thread:1");
// setName()
// st.setName("线程一");
// 设置线程的优先级
st.setPriority(Thread.MAX_PRIORITY);
st.start();
Thread.currentThread().setName("main线程");
// 设置main线程的优先级为 1
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
for (int i = 0; i < 101; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
}
// 当 i = 40,main线程调用st线程对象的join()方法
// if (i == 40) {
// try {
// st.join();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
}
// 由于在main线程中,调用了st.join(),所以导致主线程进入阻塞状态,st线程执行完毕之后cpu才有可能执行main线程
// main线程最后执行这段代码时,st线程早以销毁了。所以返回false
System.out.println(st.isAlive());
}
}
class SubThread extends Thread {
public SubThread(String name) {
super(name);
}
@Override
public void run() {
// 线程体
for (int i = 0; i < 101; i++) {
if (i % 2 == 0) {
// 默认情况下:线程的名字是Thread-0
System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
}
// if (i == 40) {
// // st线程对象释放cpu的执行权,但是cpu也有可能下一次再次执行它。
// yield();
//
// }
if (i % 40 == 0) {
try {
// 为什么只能用try catch包裹异常呢?
// 面向对象中方法重写的一个知识点:
// 当字类重写父类方法之后,字类重写方法抛出的异常不能大于父类被重写方法抛出的异常,父类中的run()方法本身没有抛出异常
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
四:线程的同步(重点)
1.理解线程的安全问题
2.卖车票的案例引出线程安全问题
package cn.xuguowen.java;
/**
* @author xuguowen
* @create 2021-02-18 14:57
* @Description 创建三个窗口买票,总票数为100张.使用实现Runnable接口的方式
* 存在线程安全问题,待解决。
*
* 1.问题:卖票过程中出现了重票和错票。--->线程安全问题
* 2.问题出现的原因:当某个线程操作车票的过程中,还没有操作完成时,其他线程参与进来,也操作车票
* 3.如何解决? 当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,
* 其他线程才可以开始操作ticket。这种情况即使线程a出现的阻塞,也不能被改变。
*/
public class SellTickets1 {
public static void main(String[] args) {
// 创建实现类对象
Window1 w1 = new Window1();
// 创建线程对象
Thread t1 = new Thread(w1, "窗口1");
Thread t2 = new Thread(w1, "窗口2");
Thread t3 = new Thread(w1, "窗口3");
// 启动窗口,卖票
t1.start();
t2.start();
t3.start();
}
}
class Window1 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
// 没有sleep()方法时,也会出现重票和错票问题,只是可能性极小
// 加上sleep()方法,大概率情况下会出现重票和错票的情况了,
// 在这里加上sleep()方法:会出现错票
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
System.out.println(Thread.currentThread().getName() + ": 卖票,票号为:" + ticket);
// 出现重票
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
} else {
break;
}
}
}
}
3.在Java中,使用同步机制解决线程安全问题—>同步代码块
3.1:使用同步代码块的方式解决上述 2 种创建线程方式出现的线程安全问题
方式一:同步代码块
synchronized (同步监视器){
// 需要被同步的代码
}
说明:1.操作共享数据的代码,就是需要被同步的代码。
2.共享数据:多个线程共同操作的变量。比如:ticket
3.同步监视器:俗称 锁。任何一个类的对象都可以充当锁。
但是:多个线程必须共用同一把锁
①:解决实现Runnable接口出现的线程安全问题
package cn.xuguowen.java;
import com.sun.org.apache.bcel.internal.generic.NEW;
/**
* @author xuguowen
* @create 2021-02-18 14:57
* @Description 创建三个窗口买票,总票数为100张.使用实现Runnable接口的方式
* 存在线程安全问题,待解决。
*
* 1.问题:卖票过程中出现了重票和错票。--->线程安全问题
* 2.问题出现的原因:当某个线程操作车票的过程中,还没有操作完成时,其他线程参与进来,也操作车票
* 3.如何解决? 当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,
* 其他线程才可以开始操作ticket。这种情况即使线程a出现的阻塞,也不能被改变。
*
* 在Jaca中,我们使用同步机制来解决线程安全问题。
* 方式一:同步代码块
* synchronized (同步监视器){
* // 需要被同步的代码
* }
*
* 说明:1.操作共享数据的代码,就是需要被同步的代码。
* 2.共享数据:多个线程共同操作的变量。比如:ticket
* 3.同步监视器:俗称 锁。任何一个类的对象都可以充当锁。
* 但是:多个线程必须共用同一把锁
*/
public class SellTickets1 {
public static void main(String[] args) {
// 创建实现类对象
Window1 w1 = new Window1();
// 创建线程对象
Thread t1 = new Thread(w1, "窗口1");
Thread t2 = new Thread(w1, "窗口2");
Thread t3 = new Thread(w1, "窗口3");
// 启动窗口,卖票
t1.start();
t2.start();
t3.start();
}
}
class Window1 implements Runnable{
private int ticket = 100;
Object obj = new Object(); // 同步监视器,必须是多个线程共用的一把锁
@Override
public void run() {
// 这是错误的:因为这样是每个线程都会拥有一把锁了
// Object obj = new Object();
while (true) {
// synchronized (obj) {
synchronized (this) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": 卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
②:解决继承Thread出现的线程安全问题
package cn.xuguowen.java;
/**
* @author xuguowen
* @create 2021-02-18 14:00
* @Description 创建三个窗口买票,总票数为100张.使用继承Thread类的方式实现
* 存在线程安全问题,待解决。
使用synchronized同步代码块解决线程安全问题
*
*/
public class SellTickets2 {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
class Window extends Thread{
private static int ticket = 100; // 总票数
// 必须使用静态变量,因为锁要唯一
private static Object object = new Object();
// 实现run()方法
@Override
public void run() {
while (true) {
// synchronized (object){
synchronized (Window.class){
if (ticket > 0) {
// 进行卖票
System.out.println(getName() + ": 卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
3.2:同步代码块的难点
①:同步监视器要保证它的唯一性。
补充: 在解决实现Runnable接口方式创建线程出现安全问题时,同步监视器可以考虑使用 this;在解决继承Thread方式创建线程出现安全问题时,同步监视器慎用 this,可以考虑使用 子类名.class获取字类对象,因为类只加载一次,所以也保证了同步监视器的唯一性。
②:同步代码块需要将操作共享数据的代码包裹起来。但是不能包裹少了,也不能包裹多了。因为包裹少了,还会存在多个线程由于某些原因操作共享数据出现的安全问题,包裹多了就达不到要求,与实际效果背道而驰。例如在本案例中,如果将整个 while 循环包裹进来的话,只能是一个线程参与卖票了,其他线程等待。
3.3:同步代码块利弊
①:解决了线程之间共享数据的安全问题。
②:局限性——操作需要被同步的代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。
4.在Java中,使用同步机制解决线程安全问题—>同步方法
同步方法:如果操作共享数据的代码完整的声明在一个方法当中,我们不妨将此方法声明为同步方法。
①:解决实现Runnable接口出现的线程安全问题
package cn.xuguowen.java;
/**
* @author xuguowen
* @create 2021-02-19 18:14
* @Description 使用同步方法解决实现Runnable接口的线程安全问题
*/
public class SellTickets3 {
public static void main(String[] args) {
Window3 w3 = new Window3();
Thread t1 = new Thread(w3, "窗口1");
Thread t2 = new Thread(w3, "窗口2");
Thread t3 = new Thread(w3, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window3 implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
private synchronized void show() { // 同步方法也用到了同步监视器:用的是this
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + ticket);
ticket--;
}
}
}
②:解决继承Thread出现的线程安全问题
package cn.xuguowen.java;
/**
* @author xuguowen
* @create 2021-02-19 20:53
* @Description 使用同步方法解决继承Thread类出现的线程安全问题
*/
public class SellTickets4 {
public static void main(String[] args) {
Window4 t1 = new Window4();
Window4 t2 = new Window4();
Window4 t3 = new Window4();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window4 extends Thread{
private static int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
// private synchronized void show() { // 此时同步监视器是 t1 t2 t3,没有解决线程安全问题
private static synchronized void show() { // 同步监视器是 Window4.class
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
}
}
**总结:**同步方法仍然涉及到同步监视器,只是不需要我们显示声明。
- 非静态同步方法:同步监视器是 this
- 静态同步方法:同步监视器是 当前类本身
5.死锁
5.1:死锁就是:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,这样就形成了死锁。
5.2:死锁出现之后:程序不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
5.3:解决方法
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
5.4:代码演示:
package cn.xuguowen.java2;
/**
* @author xuguowen
* @create 2021-02-20 10:06
* @Description 死锁:不同的线程分别占用对方所需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源
* 就形成了线程的死锁
* 出现死锁后:程序不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
* 解决方法:
* 1.专门的算法、原则
* 2.尽量减少同步资源的定义
* 3.尽量避免嵌套同步
*/
public class DeadLock {
public static void main(String[] args) {
// 为了演示死锁
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
// 1.利用匿名内部类的方式创建线程:继承Thread类
new Thread(){
@Override
public void run() {
synchronized (s1) {
s1.append("a");
s2.append("1");
// 为了让代码出现死锁的概率大大增加,我让线程进入阻塞状态,出现死锁情况
// 为什么出现了死锁情况呢?
/* 因为:此线程在拿到s1锁之后,进入了睡眠状态(阻塞),那么下一个线程有可能获取到cpu的执行权
下一个线程也需要s1这把锁,此线程正在占用这把锁,所以出现死锁问题。
*/
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2) {
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
// 2.利用匿名内部类的方式创建线程:实现Runnable接口
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2) {
s1.append("c");
s2.append("3");
// 为了让代码出现死锁的概率大大增加,我让线程进入阻塞状态
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1) {
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
6.在Java中,使用同步机制解决线程安全问题—>Lock锁
Lock:是JKD5.0新增的一个接口,通过显示定义同步锁对象来实现同步,解决线程安全问题。常用的实现类ReentrantLock。
细节都在代码中
package cn.xuguowen.java2;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author xuguowen
* @create 2021-02-20 10:44
* @Description Lock(锁):JDK5.0新增的接口,通过显示定义同步锁对象来实现同步
* 常用的实现类:ReentrantLock
*
* 面试题:synchronized 与 Lock的异同?
* 相同:都可以解决线程安全问题
* 不同:synchronized机制在执行完相应的同步代码之后,自动的释放同步监视器(锁)。
* Lock需要手动的启动锁(lock()),同时也需要手动的释放锁(unlock()),
* 并且使用Lock锁,JVM将花费较少的时间来调用线程,性能更好(体现在到boolean参数的构造器中),
* 并且具有更高的扩展性。
* 开发中如何使用?
* Lock-->同步代码块-->同步方法
*/
public class LcokTest {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w,"窗口1");
Thread t2 = new Thread(w,"窗口2");
Thread t3 = new Thread(w,"窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window implements Runnable{
private int ticket = 100;
// 实例化锁对象
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// 主动在多个线程开始访问共享资源时,为其上锁,调用lock()方法
try {
// 主动上锁,别忘记释放锁
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 {
lock.unlock();
}
}
}
}
五:线程间的通信
1.举例学习线程间的通信问题:使用两个线程打印输出1-100,线程1、线程2交替执行。
package cn.xuguowen.java3;
/**
* @author xuguowen
* @create 2021-02-21 10:54
* @Description 线程通信案例
* 使用两个线程打印输出1-100,线程1、线程2交替打印
*
* 涉及到的3 个方法
* 1.wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器
* 2.wait(long timeout) 导致当前线程等待,直到另一个线程调用 notify()方法或该对象的 notifyAll()方法,或者指定的时间已过。
* 3.wait(long timeout, int nanos) 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法,或者某些其他线程中断当前线程,或一定量的实时时间。
* 4.notify():一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程被wait,就唤醒优先级高的
* 5.notifyAll():一旦执行此方法,就会唤醒所有被wait的线程
* 说明:1.wait()、notify()、notifyAll()三个方法必须使用在同步代码块或者同步方法中
* 2.wait()、notify()、notifyAll()三个方法的调用者必须是同步代码块或者同步方法中的同步监视器,
* 否则:会出现异常非法监视器状态异常 IllegalMonitorStateException
* 3.wait()、notify()、notifyAll()三个方法定义在java.lang.Object类中。
* 首先同步监视器可以是任意对象,那意味着三个方法的调用者也可以是任意对象,任意对象都要
* 有这三个方法,所以定义在Object类中
*
* 面试题:sleep()和wait()的异同?
* 相同:执行此方法,都可以使得当前线程进入阻塞状态。
* 不同:1.声明的位置不同:sleep()方法声明在Thread类中。wait()方法声明在Object类中
* 2.调用要求不同:sleep()可以在任何需要的场景下调用。wait()必须使用在同步代码块或者同步方法中
* 3.是否释放同步监视器:如果二者都使用在同步代码块或者同步方法中,sleep()不会释放同步监视器,wait()会释放同步监视器
*/
public class ThreadCommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number, "线程1");
Thread t2 = new Thread(number, "线程2");
t1.start();
t2.start();
}
}
class Number implements Runnable{
private int number = 1;
private Object obj = new Object();
@Override
public void run() {
while (true){
synchronized (obj) { // IllegalMonitorStateException出现异常,调用方法的是当前对象this,需要一致
// 因为上一个线程进入阻塞状态后并释放了锁,所以下一个线程可以拿到锁再次进来
// 进来第一时间将上一个线程唤醒,否则会出现死锁。
obj.notify();
// 这样就实现了线程间的通信
if (number < 101) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
// wait()方法:让当前线程进入阻塞状态,并主动释放锁(同步监视器)
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
2.面试题:sleep()和wait()的异同?
相同:执行此方法,都可以使得当前线程进入阻塞状态。
不同:
- 声明的位置不同:sleep()方法声明在Thread类中。wait()方法声明在Object类中
- 调用要求不同:sleep()可以在任何需要的场景下调用。wait()必须使用在同步代码块或者同步方法中
- 是否释放同步监视器:如果二者都使用在同步代码块或者同步方法中,sleep()不会释放同步监视器,wait()会释放同步监视器
3.经典例题:生产者/消费者问题
package cn.xuguowen.java3;
/**
* @author xuguowen
* @create 2021-02-21 12:51
* @Description 线程通信应用:生产者/消费者问题
*
* 生产者将产品交给店员,而消费者从店员处取走产品。店员一次只能持有固定数量的产品(比如:20),
* 如果生产者试图生产更多的产品,店员会叫生产者停一下。如果店中有空位放置产品了,再通知生产者继续生产;
* 如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
*
* 分析:
* 1.是否是多线程问题? 是 生产者线程,消费者线程
* 2.是否有共享数据? 是 店员(或产品)
* 3.如何解决线程的安全问题?同步机制
* 4.是否涉及到线程的通信?是
*/
public class ProductorTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Productor p1 = new Productor(clerk);
p1.setName("生产者1");
p1.start();
Customer c1 = new Customer(clerk);
c1.setName("消费者1");
c1.start();
}
}
// 店员
class Clerk {
private int products = 0;
// 生产产品
public synchronized void product() {
if (products < 20) {
products++;
System.out.println(Thread.currentThread().getName() + ":开始生产第" + products + "个产品");
notify();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 消费产品
public synchronized void consume() {
if (products > 0){
System.out.println(Thread.currentThread().getName() + ":开始消费第" + products + "个产品");
products--;
notify();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 生产者
class Productor extends Thread{
private Clerk clerk;
public Productor(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
while (true) {
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":开始生产产品");
clerk.product();
}
}
}
// 消费者
class Customer extends Thread{
private Clerk clerk;
public Customer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
while (true) {
try {
sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":开始消费产品");
clerk.consume();
}
}
}
六:线程的生命周期
1.在Thread类中,提供了枚举类State,给出了线程的生命周期。JDK中是通过方法调用划分线程的生命周期。
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
2.这样理解线程的生命周期:新建—>就绪–(阻塞)->运行—>死亡。
如下图所示: