在idea中一个工程Project开一个窗口,每个工程有不同的Module
比如京东商城一个项目,不同的Module相当于不同的功能模块,如秒杀模块等等。
也就是说如果在创建一个淘宝商城需要再新打开一个窗口即新建一个Project
idea中最顶级的就是Project,工程,一个窗口只能有一个工程,打开另一个工程只能用新的窗口
第八章多线程
概述
内存图
- 程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
- 进程(process)是程序的**一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。——生命周期
如:运行中的QQ,运行中的MP3播放器
程序是静态的,进程是动态的
进程作为资源分配的单位,系统在运行时会为每个进程分配不同**的内存区域 - 线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。
若一个进程同一时间并行执行多个线程,就是支持多线程的
线程作为调度和执行的单位,**每个线程拥有独立的运行栈和程序计数器(pc),**线程切换的开销小
一个进程中的多个线程共享相同的内存单元/内存地址空间—>它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。 - 一个Java应用程序java.exe,其实至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
说明:
一个进程可能有多个线程,每一个线程都有一套虚拟机栈和程序计数器,同一个进程中的线程共享同一个方法区和堆。也就是说一个进程有一个方法区和堆,一个线程有一个虚拟机栈和程序计数器。
即每个线程,拥有自己独立的:栈、程序计数器
多个线程,共享同一个进程中的结构:方法区、堆
易混淆:
**并发:**指两个或多个事件在同一时间间隔内发生。这些时间宏观上是同时发生的,但微观上是交替发生的。
**并行:**指两个或多个事件在同一个时刻同时发生。
创建线程的方式
创建多线程的方式一
多线程的创建,方式一:继承于Thread类
- 创建一个继承于Thread类的子类
- 重写Thread类的run()-----> 将此线程执行的操作声明在run()中
- 创建Thread类的子类的对象
- 通过此对象调用start() : ①启动当前线程 ② 调用当前线程的run()
继承于Thread类的子类不可以抛异常,原因是Thread类本身就没有抛异常,同时子类抛出异常的类型不大于父类抛出的异常类型,Thread类没有抛异常,那么它的子类就不能抛异常。
举例:
//创建一个继承于Thread类的子类
public class MyThread extends Thread{
@Override
//重写Thread类的run()
public void run() {
//将此线程要执行的操作声明在run方法中
//遍历100以内的偶数
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0){
System.out.println(i);
}
}
}
}
public class MyThreadTest {
public static void main(String[] args) {
//创建子类的对象
MyThread myThread = new MyThread();
//调用start方法
myThread.start();
//我们不能通过直接调用run()的方式启动线程
//myThread.run()这是错误的。
//问题二:再启动一个线程,遍历100以内的偶数不可以还让已经start()的线程去执行。会报IllegalThreadStateException
//myThread.start();
//应该重新new 对象,再用新对象调用start方法
MyThread myThread1 = new MyThread();
myThread1.start();
//如下代码仍然是在main线程中执行
for (int i = 0; i <= 100; i++) {
if (i % 2 != 0){
System.out.println(i);
}
}
}
}
以上代码需要注意的是上面有两个线程,一个主线程执行main方法,另一个线程执行run(),且先执行主线程main(),当start()方法执行完之后,另一个线程开始执行run()方法,若此时main()方法后面还有执行语句,主线程还将继续向下执行,此时就是两个线程并行执行。
图解:
问题一:我们不能通过直接调用run()的方式启动线程。(用对象.run()的方式这时候便不再是多线程而转成单线程了,主线程执行到run会去用主线程执行run方法,执行完接着向下执行,此时自始至终只有main一个主线程。只有"对象.start"的方式才能调用新线程。)
问题二:再启动一个线程,遍历100以内的偶数不可以还让已经start()的线程去执行。会报IllegalThreadStateException。应该重新new 对象,再用新对象调用start方法
启动一个线程,必须调用start(),不能调用run()的方式启动线程。如果再启动一个线程,必须重现创建一个Thread子类的对象,调用此对象的start().
练习:练习创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数
/*
练习创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数
*/
public class ThreadExer {
public static void main(String[] args) {
//方式一
/*Thread1 thread1 = new Thread1();
Thread2 thread2 = new Thread2();
thread1.start();
thread2.start();*/
//方式二创建匿名子类
//由于只使用一次,所以也可以创建匿名子类
new Thread(){
@Override
public void run() {
//遍历偶数
for (int i = 0; i < 100; i++) {
if (i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
new Thread(){
@Override
public void run() {
//遍历奇数
for (int i = 0; i < 100; i++) {
if (i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
}
}
class Thread1 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 Thread2 extends Thread{
@Override
public void run() {
//遍历奇数
for (int i = 0; i < 100; i++) {
if (i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
线程中常用的方法
Thread中的常用方法
-
start():启动当前线程**;**调用当前线程的run()
-
run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
-
currentThread():静态方法,返回执行当前代码的线程
-
getName():获取当前线程的名字
-
setName():设置当前线程的名字
设置名字有两种方式①通过setName()方法②构造器
父类Thread中有初始化name的构造器,子类调用父类构造器即可
public Thread(String name){ this.name = name; }
-
yield():释放当前cpu的执行权。(这一时刻释放了,就看cpu如何调度了,有可能释放之后cpu就去执行其他线程了,也有可能cpu又分配给了这个线程继续执行 )。[暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程,若队列中没有同优先级的线程,忽略此方法]
-
join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞方法。[低优先级的线程也可以获得执行]
-
stop():已过时。当执行此方法时,强制结束当前线程。
-
sleep(long millitime):让当前线程 ”睡眠” 指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。
-
isAlive():判断当前线程是否存活
public class ThreadMethod {
public static void main(String[] args) {
Thread3 thread3 = new Thread3("线程1");//通过构造器命名
thread3.setName("分线程");//给线程命名
thread3.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){
try {
//join()也只能被线程调用,这时候不能省略,ThreadMethod没有继承与Thread,不是线程
thread3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//判断是否存活
System.out.println(thread3.isAlive());
}
}
class Thread3 extends Thread{
public Thread3(String name) {
super(name);
}
@Override
public void run() {
//遍历偶数
for (int i = 0; i < 100; i++) {
if (i % 2 == 0){
try {
//睡眠
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + i);
}
if (i % 20 == 0){
//释放当前cpu的执行权
//只能线程调用
yield();//省略this.yield(),且thread3是继承线程,故可以省略
}
}
}
}
线程的优先级
-
MAX_PRIORITY:10 -------> 最高优先级
MIN_PRIORITY:1 -------> 最低优先级
NORM_PRIORITY:5 -------> 默认优先级
-
如何获取和设置当前线程的优先级
getPriority():获取线程的优先级
setPriority(int p):设置线程的优先级 范围:1~10 。
**需要说明的一点是并不是线程的优先级高就一定被先执行,**优先级低的也会比优先级高的先执行,只是说在大量代码中,优先级高的被优先执行的多一些。
说明:高优先级的线程要抢占低优先级线程的cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。
例子:创建三个窗口买票,总票数为100 张
/**
* 例子:创建三个窗口买票,总票数为100 张
* 目前存在线程安全问题,待解决
*/
class Window extends Thread{
private static int ticket = 100;//必须声明为static的
//如果不声明为static,从面向对象的角度看每个对象都各自有一个ticket属性为100,显然不是我们想要的,应该声明为全局变量,所有人都一样
@Override
public void run() {
while (true){
if (ticket > 0){
System.out.println(getName() + ":买票,票号为:" + ticket);
ticket--;
}else {
break;
}
}
}
}
public class WindowTest{
public static void main(String[] args) {
//创建三个窗口
Window t1 = new Window();
Window t2 = new Window();
Window t3 = new Window();
t1.setName("线程一");
t2.setName("线程二");
t3.setName("线程三");
t1.start();
t2.start();
t3.start();
}
}
创建多线程的方式二
创建多线程的方式二:实现Runnable接口
- 创建一个实现了Runnable接口的类
- 实现类去实现Runnable中的抽象方法:run()
- 创建实现类的对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
- 通过Thread类的对象调用start()
public class ThreadTest1 {
public static void main(String[] args) {
//3.创建实现类的对象
MThread mThread = new MThread();
//4.将此对象作为参数传递到Thread类的构造器中
Thread t1 = new Thread(mThread);
//5.通过Thread类的对象调用start():①启动线程②调用当前线程的run()--->调用了Runnable类型
//的target的run() ,而我们在构造器中传递的参数便是target ----> 这都是通过源码看出来的。所以以后碰见不懂的可以去看源码。
t1.start();
//再启动一个线程,执行相同的操作
//此时只需重新new一个Thread即可,传入同一个参数mThread表示执行的操作是相同的
Thread t2 = new Thread(mThread);
t2.start();
}
}
//1.创建实现Runnable接口的类
class MThread implements Runnable{
//2.实现抽象方法run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0){
System.out.println(i);
}
}
}
}
Thread t1 = new Thread(mThread);
t1.start();
//start()执行的是当前线程的run()
//Thread中的源码
@Override
public void run() {//Thread中的run()
if (target != null) {
target.run();
}
}
//参数target是:
private Runnable target;
//而这个target就是传入的实现类的对象
//这也就解释了为什么会调用实现类的run()
使用第二种创建线程的方式买票,此时不必加static,更便于共享数据
/**
* 例子:创建三个窗口买票,总票数为100 张
* 目前存在线程安全问题,待解决
*/
class Window implements Runnable{
//不必加static,因为此时共用同一个对象
private int ticket = 100;
@Override
public void run() {
while (true){
if (ticket > 0){
System.out.println(Thread.currentThread().getName() + ":买票,票号为:" + ticket);
ticket--;
}else {
break;
}
}
}
}
public class WindowTest{
public static void main(String[] args) {
//创建三个窗口
Window t= new Window();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
t1.setName("线程一");
t2.setName("线程二");
t3.setName("线程三");
t1.start();
t2.start();
t3.start();
}
}
比较创建线程的两种方式:
开发中:优先选择:实现Runnable接口的方式
原因:1. 实现的方式没有类的单继承性的局限性
2. 实现的方式更适合来处理多个线程有共享数据的情况。
联系:public class Thread implements Runnable,即Thread类本身也实现了Runnable接口且重写了run()方法
相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
目前两种方式,想要启动线程,都是调用的Thread类中的start()
线程通信:wait() / notify() / notifyAll() : 此三个方法定义在Object类中的。
线程的分类:一种是守护线程,一种是用户线程。
如main()就是用户线程,gc()垃圾回收线程就是守护线程,当用户线程结束时,守护线程也将结束。可以通过调用方法将用户线程设置为守护线程。
线程的状态及生命周期
线程的几种状态:
- 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
需要注意的是调用start()后线程处于就绪状态此时并不一定会被执行,因为还要得到cpu的调度才会被执行。
线程的生命周期及对应的方法:
线程的安全问题
- 问题:买票过程中出现了重票、错票 — > 出现了线程的安全问题
- 问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票
- 如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。
- 在java中,我们通过同步机制,来解决线程的安全问题。
同步:指的是在这段代码中只能执行单线程,实现了线程安全。
异步:指的是同时执行多线程
方式一:同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
说明:1. 操作共享数据的代码,即为需要被同步的代码 ---- > 不能包含代码多了,也不能包含代码少了。只能把需要同步的代码包含。包含的少了,不能解决同步,包多了有时候不符合具体要求,实现的功能不对。
2. 共享数据:多个线程共同操作的变量。比如:ticket就是共享数据
3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
要求:多个线程必须要共用同一把锁。
补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
举例:
必须共用同一把锁,如果在写同步代码块的时候发现并不同步,八成就是没有共用同一把锁
//使用同步代码块解决实现Runnable接口方式的线程安全问题
class Dog{
}
class Window implements Runnable{
private int ticket = 100;
Object object = new Object();
Dog dog = new Dog();
@Override
public void run() {
// Object object = new Object();必须共用同一把锁,放在这里就不是共用同一把锁了
//如果在写同步代码块的时候发现并不同步,八成就是没有共用同一把锁
while (true){
synchronized (this){//方式二,用当前对象来充当。此时的this:唯一的Window的对象
//因为实现Runnable接口的方式只new了一个对象
//synchronized (object){//锁可以是任何一个类的对象,包括自定义一个类Dog
if (ticket > 0){
System.out.println(Thread.currentThread().getName() + ":买票,票号为:" + ticket);
ticket--;
}else {
break;
}
}
}
}
}
public class WindowTest{
public static void main(String[] args) {
//创建三个窗口
Window t= new Window();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
t1.setName("线程一");
t2.setName("线程二");
t3.setName("线程三");
t1.start();
t2.start();
t3.start();
}
}
-
同步的方式,解决了线程的安全问题。----- > 好处
操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。---- > 局限性
使用同步代码块解决继承Thread类的方式的线程安全问题:
注意:在继承Thread类创建多线程的方式中,慎用this方式充当同步监视器,考虑使用当前类充当同步监视器(目的保证唯一)
方式二:同步方法
同步方法解决线程的安全问题:
如果操作共享数据的代码**完整的声明在一个方法中,**我们不妨将此方法声明同步的。
//使用同步方法解决实现Runnable接口的线程安全问题
class Window2 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while (true){
show();
if (ticket <= 0){
break;
}
}
}
//需要将操作共享数据的代码放在一个方法中
//将此方法声明为synchronized
public synchronized void show(){//这里面也有同步监视器,这里的同步监视器是this。默认了。
if (ticket > 0){
System.out.println(Thread.currentThread().getName() + "票号:" + ticket);
ticket--;
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
Window2 window2 = new Window2();
Thread t1 = new Thread(window2);
Thread t2 = new Thread(window2);
Thread t3 = new Thread(window2);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
//使用同步方法解决继承Thread类的线程安全问题
class Window3 extends Thread{
private static int ticket = 100;
@Override
public void run() {
while (true){
show();
if (ticket <= 0){
break;
}
}
}
/**
* 这时候的同步监视器是Window3.class
* 可不是:t1,t2,t3.这种解决方式是错误的,必须唯一
* 为此我们在同步方法上加了static
*/
private static synchronized void show(){
if (ticket > 0){
System.out.println(Thread.currentThread().getName() + "票号:" + ticket);
ticket--;
}
}
}
public class WindowTest3 {
public static void main(String[] args) {
Window3 t1 = new Window3();
Window3 t2 = new Window3();
Window3 t3 = new Window3();
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
关于同步方法的总结:
-
同步方法仍然涉及到同步监视器,只是不需要我们显式的声明
-
非静态的同步方法(实现接口),同步监视器是this
静态的同步方法(继承Thread类),同步监视器是:当前类本身。
懒汉式(线程安全)
使用同步机制将单例模式中的懒汉式改写为线程安全的:
public class BankTest {
}
class Bank{
private Bank(){
}
private static Bank instance = null;
//同步方法实现,此时是静态方法,同步监视器是当前类即Bank.class
/*public static synchronized Bank getInstance(){
if (instance == null){
instance = new Bank();
}
return instance;
}*/
public static Bank getInstance(){
//使用同步代码块,此时由于是静态的只能使用当前类作为同步监视器
//方式一,效率稍差,即便instance非空,后面的线程也要一一判断
/*synchronized (Bank.class) {
if (instance == null){
instance = new Bank();
}
return instance;
}*/
//方式二,效率更高
if (instance == null){
synchronized (Bank.class) {
//里面也要进行判断,因为有可能刚开始多个线程都判断为null,进入了第一层if语句,所以在synchronized里面还要判断
if (instance == null){
instance = new Bank();
}
}
}
return instance;
}
}
死锁的问题
死锁:
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
演示线程的死锁问题
//演示线程的死锁问题
public class DeadLock {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
//匿名子类,继承的方式
new Thread(){
@Override
public void run() {
/**
* 有两把锁,先拿着s1,再执行拿着s2
*/
synchronized (s1){
s1.append("a");
s2.append("1");
//在不加sleep的情况下就有发生死锁的可能
//加上sleep只是发生死锁的可能性增大了,并不是原来没有
//发生死锁的原因就是这个线程先拿s1,另一个线程拿着s2,
// 这个线程在等待s2资源,另一个线程在等待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();
//实现接口
new Thread(new Runnable() {
@Override
public void run() {
/**
* 有两把锁,先拿着s2,再执行拿着s1
*/
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();
}
}
线程死锁的演示:这个就更为隐蔽一些,不容易发现。
//死锁线程的演示
class A {
public synchronized void foo(B b) {//同步监视器:非静态方法那就是this即当前类的对象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() {//同步监视器:类B的对象b
System.out.println("进入了A类的last方法内部");
}
}
class B {
public synchronized void bar(A a) {//同步监视器:类B的对象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() {//同步监视器:类A的对象a
System.out.println("进入了B类的last方法内部");
}
}
public class DeadLock implements Runnable {
A a = new A();
B b = new B();
public void init() {
//也就是说主线程执行需要先拿a后拿b
Thread.currentThread().setName("主线程");
// 调用a对象的foo方法
a.foo(b);
System.out.println("进入了主线程之后");
}
public void run() {
//分线程执行完要先拿b后拿a,这样就会可能发生死锁
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();
}
}
注意:并不是说有结果输出就没有发生死锁,即便有结果输出也是会发生死锁的。
死锁解决方法:
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
方式三:Lock锁
解决线程安全问题的方式三:Lock锁 -----> JDK 5. 0 新增
- 面试题:synchronized 与 Lock 的异同?
相同:二者都可以解决线程安全问题
不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())
Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
Lock只有代码块锁,synchronized有代码块锁和方法锁
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序:(只是建议按照这个顺序,当然哪种方法都是可以的)
Lock —> 同步代码块(已经进入了方法体,分配了相应资源) ----> 同步方法(在方法体之外)
[ 原因是:使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)]
Lock锁方式
1.在实现类里面实例化ReentrantLock
2.调用锁定方法lock()
3.需要实现同步的代码放在try-finally中,不管如何发生Error/或异常都要执行unlock进行解锁
4.调用解锁方法unlock()
public class LockTest {
public static void main(String[] args) {
Window4 window4 = new Window4();
Thread t1 = new Thread(window4);
Thread t2 = new Thread(window4);
Thread t3 = new Thread(window4);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
class Window4 implements Runnable{
private int ticket = 100;
//1.实例化ReentrantLock
//这里用的是实现接口的方式,如果用的是继承,则lock必须是静态(static)的,保证全局唯一
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
//放在try中
try {
//2.调用锁定方法lock()
lock.lock();
if (ticket > 0){
System.out.println(Thread.currentThread().getName() + "买票,票号是:" + ticket);
ticket--;
}else {
break;
}
} finally {
//3.调用解锁方法unlock()
lock.unlock();
}
}
}
}
需要指出的是涉及多线程问题不一定就会涉及到线程安全问题,只有当多线程访问共享数据的时候才会涉及到线程安全问题。
线程通信
调用sleep()会使得当前对象进入阻塞状态,且此时并不会释放锁;而调用wait()会使得当前对象进入阻塞状态,但此时会释放锁,其他线程便可以开始执行。
涉及到的三个方法:
wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
notifyAll():一旦执行此方法,就会唤醒所以被wait的线程。
说明:
1.wait()、notify()、notifyAll() 三个方法必须使用在同步代码块或同步方法中。
2.wait()、notify()、notifyAll() 三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则,会出现IllegalMonitorStateException异常
3.wait()、notify()、notifyAll() 三个方法是定义在java.lang.Object类中。
/**
* 线程通信的例子:使用两个线程打印1-100。线程1,线程2交替打印
*/
class Number implements Runnable {
private int i = 1;
private Object object = new Object();
@Override
public void run() {
/**
* 同步代码块中不能包多也不能包少,如果把while包进去实现的功能就变了
* 变成了一个线程执行输出1-100
*/
/*while (true) {
synchronized (this) {
notify();//前面省略了this,要保证notify的调用者是同步代码块或同步方法中的同步监视器
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (i <= 100){
System.out.println(Thread.currentThread().getName() + ":" + i);
i++;
}else {
break;
}
try {
wait();//前面省略了this,要保证wait的调用者是同步代码块或同步方法中的同步监视器
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}*/
while (true) {
synchronized (object) {
object.notify();//同步代码块或同步方法中的同步监视器
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 使用while(true),再使用if()进行在内部判断,体会这样的好处
*/
if (i <= 100){
System.out.println(Thread.currentThread().getName() + ":" + i);
i++;
}else {
break;
}
try {
object.wait();//同步代码块或同步方法中的同步监视器
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
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("线程一");
t2.setName("线程二");
t1.start();
t2.start();
}
}
面试题:sleep() 和 wait() 的异同?(重要)
1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
2.不同点:
1)两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait()
2)调用的要求不同:sleep() 可以在任何需要的场景下调用。wait() 必须使用在同步代码块或同步方法中。
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。(锁指的是同步监视器)
生产者消费者问题
线程通信的应用:经典例题:生产者/消费者问题
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
分析:
- 是否是多线程问题?是,生产者线程,消费者线程
- 是否有共享数据?是,店员(或产品)
- 如何解决线程的安全问题?同步机制,有三种方法
- 是否涉及线程的通信?是
//继承Thread类方式
class Clerk{
private int num = 0;
//生产进程
public synchronized void produce(){
while (true){
if (num < 20){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
System.out.println(Thread.currentThread().getName() + "正在生成第" + num + "个产品");
//每生产一个便可以去唤醒消费者进程
notify();//调用者是this,即Clerk的对象,main中只new了一个Clerk的对象,实现了同步监视器唯一
}else {
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消费进程
public synchronized void consume(){
while (true){
if (num > 0){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在消费第" + num + "个产品");
num--;
//每消费一个便可以去唤醒生产者进程
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() {
clerk.produce();
}
}
//消费者进程
class Consumer extends Thread{
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
clerk.consume();
}
}
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer t1 = new Producer(clerk);
Consumer t2 = new Consumer(clerk);
t1.setName("生产者1");
t2.setName("消费者1");
t1.start();
t2.start();
}
}
*/
//实现接口方式
class Clerk{
private int num = 0;
//生产进程
public synchronized void produce(){
while (true){
if (num < 20){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
System.out.println(Thread.currentThread().getName() + "正在生成第" + num + "个产品");
//每生产一个便可以去唤醒消费者进程
notify();
}else {
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消费进程
public synchronized void consume(){
while (true){
if (num > 0){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在消费第" + num + "个产品");
num--;
//每消费一个便可以去唤醒生产者进程
notify();
}else {
//等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
//生产者进程
class Producer implements Runnable{
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
clerk.produce();
}
}
//消费者进程
class Consumer implements Runnable{
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
clerk.consume();
}
}
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer producer = new Producer(clerk);
Consumer consumer = new Consumer(clerk);
Thread t1 = new Thread(producer);
Thread t2 = new Thread(consumer);
t1.setName("生产者1");
t2.setName("消费者1");
t1.start();
t2.start();
}
}
新增创建线程的方式
创建多线程的方式三
创建线程的方式三:实现Callable接口。---- > JDK 5.0新增
步骤:
1.创建一个实现Callable的实现类
2.实现call方法,将此线程需要执行的操作声明在call()中,同时call()有返回值,当不需要返回值时可以return null
3.创建Callable接口实现类的对象
4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
6.获取Callable中call方法的返回值,如果对返回值不感兴趣,返回值没有用,这一步可以忽略
//实现Callable接口方式创建线程
//1.创建一个实现Callable的实现类
class NumThread implements Callable{
//2.实现call方法,将此线程需要执行的操作声明在call()中,同时call()有返回值,当不需要返回值时可以return null
@Override
//call()是可以抛异常的,因为Callable接口是可以抛异常的
//而run()是不可以抛异常的,只能用try-catch,原因是Thread没有抛异常,Runnable也没有抛异常
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
System.out.println(Thread.currentThread().getName() + ":" + i);
}
return sum;//根据自动装箱,返回的是Integer类型的
}
}
public class ThreadNew {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
Thread t1 = new Thread(futureTask);//此时传入的参数也是Runnable类型的,因为FutureTask实现了RunnableFuture接口,而这个接口继承于Runnable
t1.setName("分线程");
t1.start();
//6.获取Callable中call方法的返回值,如果对返回值不感兴趣,返回值没有用,这一步可以忽略
try {
//get()返回值即为FutureTask构造器参数Callable实现类重写call()的返回值
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
- call()可以有返回值的
- call()可以抛出异常,被外面的操作捕获,获取异常的信息。try-catch只能处理异常,是得不到异常信息的,不知道是什么原因导致的异常。请参考项目三开发人员调度软件中抛出了很多种不同的异常,每种都有对应的异常信息
- Callable是支持泛型的。
创建多线程的方式四
在开发中经常使用的创建线程的方式是线程池的方式,而以后学了框架会更快
创建线程的方式四:使用线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
好处:
1.提高响应速度(减少了创建新线程的时间)
2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
3.便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
线程池相关的API
- JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors
- ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
Future submit(Callable task):执行任务,有返回值,一般又来执行Callable
void shutdown() :关闭连接池 - Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
线程池创建线程:
1.提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
NumberThread numberThread = new NumberThread();
service.execute(numberThread);
3.关闭连接池
class NumberThread implements Runnable{
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
//实现不同的功能
class NumberThread1 implements Runnable{
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1.提供指定线程数量的线程池
ExecutorService service1 = Executors.newFixedThreadPool(10);
//设置线程池的属性,因为ExecutorService是接口,所以要设置线程池的属性应该先强转成对应的 类对象
//System.out.println(service.getClass());//通过getClass()方法得到所属的类ThreadPoolExecutor,这个类实现了ExecutorService接口
ThreadPoolExecutor service = (ThreadPoolExecutor)service1;
service.setCorePoolSize(15);//设置属性
//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
NumberThread numberThread = new NumberThread();
NumberThread1 numberThread1 = new NumberThread1();
service.execute(numberThread);
service.execute(numberThread1);
//3.关闭连接池
service.shutdown();
// service.execute();适合适用于Runnable,没有返回值
// service.submit();适合适用于Callable,有返回值
}
}
面试题:创建多线程有几种方式? ------ > 四种
只要提到生命周期关注两个概念:状态、相应的方法
关注:状态a —> 状态b :状态转换后哪些方法执行了(称为回调方法)
某个方法主动调用:导致从状态a---->状态b
对于线程来说阻塞是临时状态,不可以作为最终状态;死亡才是最终状态
释放锁的操作
1.当前线程的同步方法、同步代码块执行结束。
2.当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行(方法执行结束肯定会释放锁)。
3.当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束(出现异常线程都结束了,肯定也会释放锁)。
4.当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
不会释放锁的操作
1.线程执行同步代码块或同步方法时,程序调用Thread.sleep()、 Thread.yield()方法暂停当前线程的执行
2.线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。