一、并发、并行、进程、线程概念
并发与并行
- 并发:指两个或多个事件在同一个时间段内发生。
- 并行:指两个或多个事件在同一时刻发生(同时发生)。
在操作系统中,安装了多个程序,并行指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。
而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核 越多,并行处理的程序越多,能大大的提高电脑运行的效率。
注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。
线程与进程
- 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
- 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
简而言之:**一个程序运行后至少有一个进程,一个进程中可以包含多个线程 **
我们可以再电脑底部任务栏,右键----->打开任务管理器,可以查看当前任务的进程:
进程
线程
线程调度:
- 分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。 - 抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
大部分操作系统都支持多进程并行运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。
实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。
二、创建线程
继承Thread类
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程
示例:
public class MyThread extends Thread {
//定义指定线程名称的构造方法
public MyThread(String name) {
//调用父类的String参数的构造方法,指定线程的名称
super(name);
}
/**
* 重写run方法,完成该线程执行的逻辑
*/
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println(getName()+":"+i);
}
}
}
测试:
public class Demo1 {
public static void main(String[] args) {
//创建自定义线程对象
MyThread mt = new MyThread("新建的线程");
//开启新线程
mt.start();
//在主方法中执行for循环
for (int i = 0; i < 10; i++) {
System.out.println("主线程:"+i);
}
}
}
实现Runnable接口
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动线程。
示例:
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
测试:
public class Demo2 {
public static void main(String[] args) { //创建自定义类对象 线程任务对象
MyRunnable mr = new MyRunnable();
//创建线程对象
Thread t = new Thread(mr, "新建的线程");
t.start();
for (int i = 0; i < 20; i++) {
System.out.println("主线程" + i);
}
}
}
继承Thread 和实现Runnable的区别
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。总结:实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
三、线程常用方法
方法名 | 说明 |
---|---|
public static void sleep(long millis) | 当前线程主动休眠 millis 毫秒。 |
public static void yield() | 当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片。 |
public final void join() | 允许其他线程加入到当前线程中。 |
public void setPriority(int) | 线程优先级为1-10,默认为5,优先级越高,表示获取CPU机会越多。 |
public void setDaemon(boolean) | 设置为守护线程线程有两类:用户线程(前台线程)、守护线程(后台线程) |
线程的优先级
- 我们可以通过传递参数给线程的
setPriority()
来设置线程的优先级别 - 调整线程优先级:Java线程有优先级,优先级高的线程会获得较多的运行机会。优先级 : 只能反映 线程 的 中或者是 紧急程度 , 不能决定 是否一定先执行
Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:
static int MAX_PRIORITY
线程可以具有的最高优先级,取值为10。
static int MIN_PRIORITY
线程可以具有的最低优先级,取值为1。
static int NORM_PRIORITY
分配给线程的默认优先级,取值为5。
示例:
/**
* 优先级
*
*/
public class PriorityThread extends Thread{
@Override
public void run() {
for(int i=0;i<50;i++) {
System.out.println(Thread.currentThread().getName()+"============"+i);
}
}
}
测试:
public class TestPriority {
public static void main(String[] args) {
PriorityThread p1=new PriorityThread();
p1.setName("p1");
PriorityThread p2=new PriorityThread();
p2.setName("p2");
PriorityThread p3=new PriorityThread();
p3.setName("p3");
p1.setPriority(1);
p3.setPriority(10);
//启动
p1.start();
p2.start();
p3.start();
}
}
线程的休眠
使用线程的 sleep()
可以使线程休眠指定的毫秒数,在休眠结束的时候继续执行线程
示例:
class SleepThread extends Thread
{
@Override
public void run()
{
String[] names = new String[]{"zs","ls","ww","z6"};
int index = (int)(Math.random()*4);
for (int i = 3;i > 0;i--)
{
System.out.println(i);
try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println("倒计时:"+i);
}
System.out.println("抽中学员为:"+names[index]);
}
}
测试:
public class TestSleep {
public static void main(String[] args)
{
new SleepThread().start();
}
}
线程的让步
Thread.yield()
方法作用是:暂停当前正在执行的线程对象(及放弃当前拥有的cup资源),并执行其他线程。yield()
做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()
的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证**yield()**
达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。- 案例:创建两个线程A,B,分别各打印1000次,从1开始每次增加1,其中B一个线程,每打印一次,就yield一次,观察实验结果.
示例:
class Task1 implements Runnable{
@Override
public void run() {
for (int i = 0;i < 200;i++){
System.out.println("A:"+i);
}
}
}
class Task2 implements Runnable{
@Override
public void run() {
for (int i = 0;i < 10;i++){
System.out.println("B:"+i);
Thread.yield();
}
}
}
public class Demo {
public static void main(String[] args) {
new Thread(new Task2()).start();
new Thread(new Task1()).start();
}
}
sleep()和yield()的区别
sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;
yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程
另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield() 方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。
线程的合并
Thread
中,join()
方法的作用是调用线程等待该线程完成后,才能继续往下运行。- join是Thread类的一个方法,启动线程后直接调用,即join()的作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。
- 为什么要用join()方法
在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。
示例:
class JoinThread extends Thread{
public JoinThread(String name){
super(name);
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程开始运行");
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"-->子线程:"+i);
}
System.out.println(Thread.currentThread().getName()+"线程结束运行");
}
}
public class JoinDemo {
public static void main(String[] args) {
System.out.println("主线程开始运行。。。");
JoinThread t1 = new JoinThread("新加入的线程");
t1.start();
// try {
// t1.join();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
System.out.println("主线程开始结束。。。");
}
}
四、守护线程
守护线程.setDaemon(true):设置守护线程
线程有两类:用户线程(前台线程)、守护线程(后台线程)
如果程序中所有前台线程都执行完毕了,后台线程会自动结束
垃圾回收器线程属于守护线程
public class DeamonThread extends Thread {
@Override
public void run() {
for(int i=0;i<50;i++) {
System.out.println(Thread.currentThread().getName()+"----------"+i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
测试:
public class TestDeamon {
public static void main(String[] args) {
//创建线程(默认前台线程)
DeamonThread d1=new DeamonThread();
//设置线程为守护线程
d1.setDaemon(true);//主线程结束便结束了
d1.start();
for(int i=0;i<10;i++) {
System.out.println("主线程:----------"+i);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
五、线程生命周期
五种基本状态
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。
新建状态(New)
当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
就绪状态(Runnable)
当调用线程对象的start()方法(t.start();
),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()
此线程立即就会执行;
运行状态(Running)
当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
阻塞状态(Blocked)
处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
2.同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3.其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
死亡状态(Dead)
线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
多线程状态之间的转换
就绪状态转换为运行状态:当此线程得到处理器资源;
运行状态转换为就绪状态:当此线程主动调用yield()方法或在运行过程中失去处理器资源。
运行状态转换为死亡状态:当此线程线程执行体执行完毕或发生了异常。
此处需要特别注意的是:当调用线程的yield()方法时,线程从运行状态转换为就绪状态,但接下来CPU调度就绪状态中的哪个线程具有一定的随机性,因此,可能会出现A线程调用了yield()方法后,接下来CPU仍然调度了A线程的情况。
六、线程安全
为什么会出现线程安全问题?
- 线程不安全:
- 当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致。
- 临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性。
- 原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
示例:
class TicketRunnable implements Runnable{
private int ticket=100;
//每个窗口卖票的操作
//窗口 永远开启
@Override
public void run() {
while(true){//有票可以卖
//出票操作
if(ticket>0){
//使用sleep模拟一下出票时间 //模拟一下出票的时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在卖票:"+ticket--);
}
}
}
}
public class ThreadSafe {
public static void main(String[] args) throws Exception{
TicketRunnable t = new TicketRunnable();
Thread t1 = new Thread(t,"窗口1");
Thread t2 = new Thread(t,"窗口2");
Thread t3 = new Thread(t,"窗口3");
//3个窗口同时卖票
t1.start();
t2.start();
t3.start();
}
}
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。那么怎么去使用呢?有三种方式完成同步操作:
- 同步代码块。
- 同步方法。
- 锁机制。
同步代码块
语法:
synchronized(临界资源对象){ //对临界资源对象加锁
//代码(原子操作)
}
同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
- 锁对象 可以是任意类型。
- 多个线程对象 要使用同一把锁。
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。
示例:
package com.qf.sync;
class Ticket2 implements Runnable{
private int ticket=100;
Object lock = new Object();
//每个窗口卖票的操作
//窗口 永远开启
@Override
public void run() {
while(true){//有票可以卖
synchronized(lock){//synchronized (this) {//this ---当前对象
if(ticket>0){
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在卖票:"+ticket--);
}
}
}
}
}
public class TicketDemo2 {
public static void main(String[] args) {
Ticket2 ticket2 = new Ticket2();
Thread t1 = new Thread(ticket2,"窗口1");
Thread t2 = new Thread(ticket2,"窗口2");
Thread t3 = new Thread(ticket2,"窗口3");
//3个窗口同时卖票
t1.start();
t2.start();
t3.start();
}
}
同步方法
同步方法 :使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
语法:
synchronized 返回值类型 方法名称(形参列表){ //对当前对象(this)加锁
// 代码(原子操作)
}
- 只有拥有对象互斥锁标记的线程,才能进入该对象加锁的同步方法中。
- 线程退出同步方法时,会释放相应的互斥锁标记。
- 如果方式是静态,锁是类名.class。
示例:
class Ticket3 implements Runnable{
private int ticket=100;
//Object lock = new Object();
//每个窗口卖票的操作
//窗口 永远开启
@Override
public void run() {
while(true){//有票可以卖
sellTicket();
if(ticket<=0){
break;
}
}
}
/**
* 锁对象,谁调用这个方法,就是谁
* 隐含锁对象,就是this
*
* 静态方法,隐含锁对象就是Ticket3.class
*/
public synchronized void sellTicket(){
if(ticket>0){
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在卖票:"+ticket--);
}
}
}
public class TicketDemo3 {
public static void main(String[] args) {
Ticket3 ticket3 = new Ticket3();
Thread t1 = new Thread(ticket3,"窗口1");
Thread t2 = new Thread(ticket3,"窗口2");
Thread t3 = new Thread(ticket3,"窗口3");
//3个窗口同时卖票
t1.start();
t2.start();
t3.start();
}
}
synchronized注意点
同步锁是谁?
对于非static方法,同步锁就是this。
对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
Lock
- JDK5加入,与synchronized比较,显示定义,结构更灵活。
- 提供更多实用性方法,功能更强大、性能更优越。
常用方法:
方法名 | 描述 |
---|---|
void lock() | 获取锁,如锁被占用,则等待。 |
boolean tryLock() | 尝试获取锁(成功返回true。失败返回false,不阻塞)。 |
void unlock() | 释放锁。 |
ReentrantLock:
- Lock接口的实现类,与synchronized一样具有互斥锁功能。
示例:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyList {
//创建锁
private Lock lock = new ReentrantLock();
private String[] str = {"A","B","","",""};
private int count = 2;
public void add(String value){
//当没有锁的时候,会出现覆盖的情况
str[count] = value;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
System.out.println(Thread.currentThread().getName()+"添加了"+value);
// lock.lock();
// try {
// str[count] = value;
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// count++;
// System.out.println(Thread.currentThread().getName()+"添加了"+value);
// }finally {
// lock.unlock();
// }
}
public String[] getStr(){
return str;
}
}
测试:
public class TestMyList {
public static void main(String[] args) throws InterruptedException {
MyList myList = new MyList();
//
Thread t1 =new Thread(new Runnable() {
@Override
public void run() {
myList.add("hello");
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
myList.add("world");
}
});
t2.start();
t1.join();
t2.join();
String[] str = myList.getStr();
for (String s : str) {
System.out.println("s:"+s);
}
}
}
七、线程通信
概述
多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。
为什么要处理线程间通信
多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
如何保证线程间通信有效利用资源
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。
等待唤醒机制
什么是等待唤醒机制
这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。
就是在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。
wait/notify 就是线程间的一种协作机制。
线程通信方法
方法 | 说明 |
---|---|
public final void wait() | 释放锁,进入等待队列 |
public final void wait(long timeout) | 在超过指定的时间前,释放锁,进入等待队列 |
public final void notify() | 随机唤醒、通知一个线程 |
public final void notifyAll() | 唤醒、通知所有线程 |
注意:所有的等待、通知方法必须在对加锁的同步代码块中。
等待唤醒机制就是用于解决线程间通信的问题的,使用到的3个方法的含义如下:
- wait:线程不再活动,不再参与调度,进入 wait set(锁池) 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中
wait(long m):wait方法如果在指定的毫秒之后,还没有被notify唤醒,就会自动醒来
sleep(long m):不会释放锁 - notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先入座。
- notifyAll:则释放所通知对象的 wait set 上的全部线程。
示例:
/*
等待唤醒案例:
1,创建一个顾客线程(消费者):告知老板要的包子种类和数量,调用wait方法,放弃cpu的执行,进入等待状态
2,创建一个老板线程(生产者):花了5秒做包子,做好包子之后,调用notify方法,通知顾客吃包子,
注意:
顾客和老板线程必须使用同步代码块包裹起来,保证等待和唤醒只能有一个在执行
同步使用的锁对象必须是唯一的,
只有锁对象才能调用wait方法和notify方法
*/
public class Demo1 {
public static void main(String[] args) {
//创建锁对象,保证唯一
Object obj =new Object();
//创建顾客线程
new Thread(){
@Override
public void run() {
while(true){
//保证等待和唤醒只能有一个在执行
synchronized (obj){
System.out.println("告知老板要的包子种类和数量");
//进入等待
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒之后执行的代码
System.out.println("拿到包子,开始吃。。。");
System.out.println("---------------------");
}
}
}
}.start();
//创建老板线程
new Thread(){
@Override
public void run() {
while(true){
//花5秒钟做包子,
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//保证等待和唤醒只能有一个在执行
synchronized (obj){
System.out.println("包子做好了。。。。");
//做好包子之后,调用notify方法,通知顾客吃包子,
obj.notify();
}
}
}
}.start();
}
}
注意:
哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。
总结如下:
- 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
- 否则,从 wait set 出来,又进入 entry set,线程就从 WAITING 状态又变成 BLOCKED 状态
调用wait和notify方法需要注意的细节
- wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
- wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
- wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。
八、死锁
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
示例:
package com.qf.safe;
public class DeadLockDemo {
private static Object lock1 = new Object();//锁1,资源1
private static Object lock2 = new Object();//锁2,资源2
public static void main(String[] args) {
//启动一个线程
new Thread(new Runnable() {
@Override
public void run() {
synchronized(lock1){
System.out.println(Thread.currentThread().getName()+"拿到了锁1,资源1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"等待锁2,资源2");
synchronized (lock2){
System.out.println(Thread.currentThread().getName()+"拿到了锁2,资源2");
}
}
}
},"线程1").start();
//产生死锁的线程
// new Thread(new Runnable() {
// @Override
// public void run() {
// synchronized(lock2){
// System.out.println(Thread.currentThread().getName()+"拿到了锁2,资源2");
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// System.out.println(Thread.currentThread().getName()+"等待锁1,资源1");
// synchronized (lock1){
// System.out.println(Thread.currentThread().getName()+"拿到了锁1,资源1");
// }
// }
// }
// },"线程2").start();
}
}
线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过Thread.sleep(1000);
让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。
破坏死锁
//破坏死锁
new Thread(new Runnable() {
@Override
public void run() {
synchronized(lock1){
System.out.println(Thread.currentThread().getName()+"拿到了锁1,资源1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"等待锁2,资源2");
synchronized (lock2){
System.out.println(Thread.currentThread().getName()+"拿到了锁2,资源2");
}
}
}
},"线程2").start();
线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。
九、线程池
概述
我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?
在Java中可以通过线程池来达到这样的效果。
**线程池:**其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
合理利用线程池能够带来三个好处:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
线程池的使用
Java里面线程池的顶级接口是java.util.concurrent.Executor
,但是严格意义上讲Executor
并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService
。
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors
线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。
Java类库提供了许多静态方法来创建一个线程池:
Executors类中创建线程池的方法如下:
a、newFixedThreadPool
创建一个固定长度的线程池,当到达线程最大数量时,线程池的规模将不再变化。
b、newCachedThreadPool
创建一个可缓存的线程池,如果当前线程池的规模超出了处理需求,将回收空的线程;当需求增加时,会增加线程数量;线程池规模无限制。
c、newSingleThreadPoolExecutor
创建一个单线程的Executor,确保任务对了,串行执行
d、newScheduledThreadPool
创建一个固定长度的线程池,而且以延迟或者定时的方式来执行,类似Timer;
使用线程池中线程对象的步骤:
- 创建线程池对象。
- 创建Runnable接口子类对象。(task)
- 提交Runnable接口子类对象。(take task)
获取到了一个线程池ExecutorService 对象,定义了一个使用线程池对象的方法如下:
public Future<?> submit(Runnable task)
:获取线程池中的某一个线程对象,并执行
Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。
- 关闭线程池(一般不做)。
示例:
package com.qf.threadpool;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
class MyThread implements Runnable{
@Override
public void run() {
System.out.println("我要一个教练");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("教练来了:"+Thread.currentThread().getName());
System.out.println("教完后,教练回到了游泳池");
}
}
public class ThreadPoolDemo {
public static void main(String[] args) {
// //创建一个包含固定数量的线程池对象
// ExecutorService executorService = Executors.newFixedThreadPool(2);
// //创建一个包含单条线程的线程池
// ExecutorService executorService = Executors.newSingleThreadExecutor();
// //创建一个带缓冲区的线程池,会根据需求创建线程
// ExecutorService executorService = Executors.newCachedThreadPool();
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
//创建Runnable实例对象
MyThread r = new MyThread();
//自己创建线程的方式
// Thread t = new Thread(r);
// t.start();
// //从线程池中获取线程对象,然后调用MyThread的run方法
// executorService.submit(r);
// //再获取一个线程对象,
// executorService.submit(r);
// executorService.submit(r);
// //注意:submit方法调用后,程序并不终止,因为线程次控制了线程的关闭
// //使用完,又归还到了线程池中,
//
// //关闭线程池
// executorService.shutdown();
for (int i = 0; i < 10; i++) {
scheduledExecutorService.schedule(r,10, TimeUnit.SECONDS);//延迟10秒执行
}
scheduledExecutorService.shutdown();;//执行到此处并不会马上关闭连接池
// while(!scheduledExecutorService.isTerminated()){
//
// }
System.out.println("Main Thread finished at"+new Date());
}
}
Callable接口
一般情况下,使用Runnable接口、Thread实现的线程我们都是无法返回结果的。但是如果对一些场合需要线程返回的结果。就要使用用Callable、Future这几个类。Callable只能在ExecutorService的线程池中跑,但有返回结果,也可以通过返回的Future对象查询执行状态。Future 本身也是一种设计模式,它是用来取得异步任务的结果
看看其源码:
public interface Callable<V> {
V call() throws Exception;
}
它只有一个call方法,并且有一个返回V,是泛型。可以认为这里返回V就是线程返回的结果。
ExecutorService接口:线程池执行调度框架
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
示例:
import java.util.Random;
import java.util.concurrent.*;
class HandleCallable implements Callable<Integer> {
private String name;
public HandleCallable(String name) {
this.name = name;
}
@Override
public Integer call() throws Exception {
System.out.println("task"+ name + "开始进行计算");
Thread.sleep(3000);
int sum = new Random().nextInt(300);
int result = 0;
for (int i = 0; i < sum; i++)
result += i;
return result;
}
}
public class FutureTest{
public static void main(String[] args) {
System.out.println("main Thread begin at:"+ System.nanoTime());
//创建线程池对象
ExecutorService executor = Executors.newCachedThreadPool();
HandleCallable task1 = new HandleCallable("1");
HandleCallable task2 = new HandleCallable("2");
HandleCallable task3 = new HandleCallable("3");
//执行
Future<Integer> result1 = executor.submit(task1);
Future<Integer> result2 = executor.submit(task2);
Future<Integer> result3 = executor.submit(task3);
executor.shutdown();
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
//获取到返回的直接
try {
System.out.println("task1运行结果:"+result1.get());
System.out.println("task2运行结果:"+result2.get());
System.out.println("task3运行结果:"+result3.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("main Thread finish at:"+ System.nanoTime());
}
}
十、线程安全集合
CopyOnWriteArrayList[重点]
- 线程安全的ArrayList,加强版读写分离。
- 写有锁,读无锁,读写之间不阻塞,优于读写锁。
- 写入时,先copy一个容器副本、再添加新元素,最后替换引用。
- 使用方式与ArrayList无异。
示例:
public class TestCopyOnWriteArrayList {
public static void main(String[] args) {
//1创建集合
CopyOnWriteArrayList<String> list=new CopyOnWriteArrayList<>();
//2使用多线程操作
ExecutorService es=Executors.newFixedThreadPool(5);
//3提交任务
for(int i=0;i<5;i++) {
es.submit(new Runnable() {
@Override
public void run() {
for(int j=0;j<10;j++) {
list.add(Thread.currentThread().getName()+"...."+new Random().nextInt(1000));
}
}
});
}
//4关闭线程池
es.shutdown();
while(!es.isTerminated()) {}
//5打印结果
System.out.println("元素个数:"+list.size());
for (String string : list) {
System.out.println(string);
}
}
}
CopyOnWriteArrayList如何做到线程安全的
CopyOnWriteArrayList使用了一种叫写时复制的方法,当有新元素添加到CopyOnWriteArrayList时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。
当有新元素加入的时候,如下图,创建新数组,并往新数组中加入一个新元素,这个时候,array这个引用仍然是指向原数组的。
当元素在新数组添加成功后,将array这个引用指向新数组。
CopyOnWriteArrayList
的整个add操作都是在锁的保护下进行的。 这样做是为了避免在多线程并发add的时候,复制出多个副本出来,把数据搞乱了,导致最终的数组数据不是我们期望的。
CopyOnWriteArrayList
的add
操作的源代码如下:
public boolean add(E e) {
//1、先加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//2、拷贝数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
//3、将元素加入到新数组中
newElements[len] = e;
//4、将array引用指向到新数组
setArray(newElements);
return true;
} finally {
//5、解锁
lock.unlock();
}
}
由于所有的写操作都是在新数组进行的,这个时候如果有线程并发的写,则通过锁来控制,如果有线程并发的读,则分几种情况:
1、如果写操作未完成,那么直接读取原数组的数据;
2、如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
3、如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。
可见,CopyOnWriteArrayList
的读操作是可以不用加锁的。
CopyOnWriteArraySet
示例:
public class TestCopyOnWriteArraySet {
public static void main(String[] args) {
//1创建集合
CopyOnWriteArraySet<String> set=new CopyOnWriteArraySet<>();
//2添加元素
set.add("pingguo");
set.add("huawei");
set.add("xiaomi");
set.add("lianxiang");
set.add("pingguo");
//3打印
System.out.println("元素个数:"+set.size());
System.out.println(set.toString());
}
}
ConcurrentHashMap[重点]
- 初始容量默认为16段(Segment),使用分段锁设计。
- 不对整个Map加锁,而是为每个Segment加锁。
- 当多个对象存入同一个Segment时,才需要互斥。
- 最理想状态为16个对象分别存入16个Segment,并行数量16。
- 使用方式与HashMap无异。
示例:
public class TestConcurrentHashMap {
public static void main(String[] args) {
//1创建集合
ConcurrentHashMap<String, String> hashMap=new ConcurrentHashMap<String, String>();
//2使用多线程添加数据
for(int i=0;i<5;i++) {
new Thread(new Runnable() {
@Override
public void run() {
for(int k=0;k<10;k++) {
hashMap.put(Thread.currentThread().getName()+"--"+k, k+"");
System.out.println(hashMap);
}
}
}).start();
}
}
}