一、 进程与线程
1、进程与线程的概念
1.1 进程
如下图所示,我们可以认为计算机中运行的每一个.exe文件都是一个进程,拥有属于自己独立的一块内存空间,是竞争计算机资源的基本单位。
1.2 线程
线程是进程中的一个执行单位(控制单元),一个线程只能属于一个进程,而一个进程可以拥有多个线程。
2、用户线程与守护线程
2.1 用户线程
通过Thread.setDaemon(false)设置为用户线程。当一个进程不包含任何存活的用户线程时,进程就会结束。
2.2 守护线程
通过Thread.setDaemon(true)设置为守护线程。顾名思义,守护线程就是用来守护用户线程的,当最后一个用户线程结束时,所有守护线程自动死亡。
注意,线程属性的设置要在线程启动之前,否则会报IllegalThreadStateException异常。如果不设置线程属性,那么默认为用户线程。
将用户线程设置为守护线程的格式为:Thread 类的对象名.setDamon(true);
二、同步与异步,并发与并行
1、线程的同步与异步
1.1 线程同步
线程同步就是当多个线程访问同一资源时,线程之间排队执行,效率低,但是可以保证数据的安全性。
1.2 线程异步
线程异步指当多个线程访问同一资源时同时执行,效率较高,但是数据的安全性不高。
2、线程的并发与并行
2.1 并发
指两个或多个线程在**同一时段内**运行
2.2 并行
指两个或多个线程在**同一时刻**同时运行
三、 创建线程的三种方式
1、Thread
继承Thread类,重写run()方法。
步骤简介:
- 编写一个Thread类的子类。
- 重写run()方法,在run()方法中编写线程任务。
- 通过子类对象调用.start()方法进行启动线程。
代码示例:
1:编写子类、重写run()方法。
package com.java.demo1;
public class MyThread extends Thread {
/**
* run方法就是线程要执行的任务方法
*/
@Override
public void run() {
//这里的代码就是一条新的执行路径
//这个执行路径的触发方式不是调用run方法,
// 而是通过thread对象的start()方法来启动任务
for(int x=0; x<10; x++){
System.out.println("很爱很爱你"+x);
}
}
}
2: 通过子类对象调用.start()方法进行启动线程。
package com.java.demo1;
/**
* 实现多线程的技术一:继承MyThread类
*
*/
public class ThreadTest {
public static void main(String[] args){
//
MyThread my = new MyThread();
my.start();
for(int i=0; i<10; i++){
System.out.println("是吗是吗"+i);
}
}
}
2、Runnable
实现Runnable接口,重写run()方法。
步骤简介:
- 创建一个任务对象(实现Runnable接口)
- 创建一个线程并为其分配任务(Thread类)
- 执行这个任务(start()方法)
代码示例:
1:创建任务
package com.java.demo1;
public class MyRunnable implements Runnable{
@Override
public void run() {
//线程的任务
for(int i=0; i<10; i++){
System.out.println("爱你"+i);
}
}
}
2:创建线程并实现
package com.java.demo1;
/**
* 实现多线程的技术二:实现Runnable类
*/
public class RunnableTest {
public static void main(String[] args) {
//实现Runnable
//1. 创建一个任务对象
MyRunnable my = new MyRunnable();
//2. 创建一个线程,并为其分配一个任务
Thread r = new Thread(my);
//3. 执行这个线程
r.start();
for(int i=0; i<10; i++){
System.out.println("喜欢你"+i);
}
}
}
3、Callable
实现Callable接口,重写call方法
上面两种方法是常用的创建线程的方法,这种相对而言不常用。Callable接口实际上是属于Executor框架中的功能类,Callable接口与Runnable接口的功能类似,但提供了比Runnable更加强大的功能:Callable可以提供返回值,而Runnable不可以;call()方法可以抛出异常而run()方法不可以。
四、Thread类的常用方法
1、获取和设置线程名称
1.1 获取线程名称
方法: Thread.currentThread().getName()
public class MyThreadTest1 {
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
System.out.println("这是子线程:"+Thread.currentThread().getName());
}
}.start();
System.out.println("这是主线程:"+Thread.currentThread().getName());
}
}
1.2 设置线程名称
1.2.1 setName()方法
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start();
t1.setName("线程1");
System.out.println("线程的名字是:"+t1.getName());
}
1.2.2 使用构造函数设置名称
public class MyThread1 extends Thread
{
MyThread1(String name){
super(name);
}
public void run(){
System.out.println("线程启动中");
}
public static void main(String[] args) {
MyThread1 t1=new MyThread1("一个线程");
t1.start();
System.out.println("Thread Name: "+t1.getName());
}
}
2、线程休眠
.sleep()方法
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
3、线程中断
interrupt()方法
public class Demo {
public static void main(String[] args) throws InterruptedException {
//线程的中断
//一个线程是一个独立的执行路径,它是否应该结束,应该由其自身决定
Thread t1 = new Thread(new MyRunnable());
//把t1设置为守护线程(一定要在启动之前设置)
t1.setDaemon(true);
t1.start();
for(int i=0; i<5; i++){
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//给线程t1添加中断标记,当main线程运行结束时,让t1线程运行catch块中的代码
t1.interrupt();
}
static class MyRunnable implements Runnable{
@Override
public void run() {
for(int i=0; i<10; i++){
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("发现了中断标记, 让t1线程通过return结束run方法进行自杀");
return;
}
}
}
}
}
五、线程安全
当存在两个或以上线程对象共享同一资源时容易出现线程安全问题,下面将以三个不同的窗口(线程)同时卖同一种票(资源)为例子,分别使用三种不同方法解决线程安全问题。
1、同步代码块
即有synchronized关键字修饰的语句块。 被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
格式:
synchronized(锁对象){需要同步的代码 }
在这个案例中,我们应该把if语句上锁,即当某线程在运行if语句时,其它线程需要等待锁对象的标记消失
注意:
- 任何对象都可以作为锁对象存在,但是多个线程执行某同步代码时应该共用同一把锁
- 如果一个线程在同步代码块中休眠了,那它就不会释放锁对象
- 锁对象不能放在run方法中创建,因为这样每个线程都会创建新的锁对象,不能实现排队机制
public class SaveTest1 {
public static void main(String[] args) {
Runnable run = new MyRunnable1();
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
Thread t3 = new Thread(run);
t1.start();
t2.start();
t3.start();
}
static class MyRunnable1 implements Runnable{
//票数
int count = 10;
//锁对象
Object o = new Object();
@Override
public void run() {
// Object o = new Object(); 注意:锁对象不能放在run方法中创建,因为这样每个线程都会创建新的锁对象,不能实现排队机制
while(true){
//给if语句上锁
synchronized(o){
if(count>0){
System.out.println("准备卖票中");
//延长卖票时间(休眠)让不安全问题出现的概率增大(可省略)
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
//打印线程名字和余票,便于观察
System.out.println(Thread.currentThread().getName()+"出票成功,当前余票为:"+count);
}else{
break;
}
}
}
}
}
}
2、同步方法
即被synchronized关键字修饰的方法。 被该关键字修饰的方法会自动被加上内置锁,从而实现同步。
格式: synchronized 返回值类型 方法名(){ }
在这个案例中,我们应该把if语句抽成方法,并用synchronized修饰该方法
注意:
- 与这个方法相比,同步代码块的方法最小可以精确到一行代码,但是这个方法最小为方法
- 锁对象:谁调用这个方法,谁就是锁对象,一般情况为this,如果方法被静态修饰,则锁对象为类名.class
- 如果同步代码块和同步方法同时存在,使用的是同一个锁对象,因此同步代码块和同步方法不能被同时运行
public class SaveTest2 {
public static void main(String[] args) {
Runnable run = new MyRunnable2();
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
Thread t3 = new Thread(run);
t1.start();
t2.start();
t3.start();
}
static class MyRunnable2 implements Runnable{
//票数
int count = 10;
@Override
public void run() {
while(true){
boolean flag = sale();
if(!flag){
break;
}
}
}
//把需要上锁的if语句抽成一个方法,并用synchronized修饰方法
public synchronized boolean sale() {
if (count > 0) {
System.out.println("准备卖票中");
//延长卖票时间(休眠)让不安全问题出现的概率增大(可省略)
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
//打印线程名字和余票,便于观察
System.out.println(Thread.currentThread().getName() + "出票成功,当前余票为:" + count);
return true;
}
return false;
}
}
}
3、显示锁
显示锁:Lock 子类:ReetrantLock
在要排队进行的代码之前通过ReetrantLock对象调用lock()方法上锁,在要排队进行的代码之后调用unlock()方法开锁
public class SaveTest3 {
public static void main(String[] args) {
Runnable run = new MyRunnable();
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
Thread t3 = new Thread(run);
t1.start();
t2.start();
t3.start();
}
static class MyRunnable implements Runnable{
//票数
int count = 10;
//显示锁
//创建锁的时候让fair参数为true,就可以使其成为一个公平锁
private Lock l = new ReentrantLock(true);
@Override
public void run() {
while(true){
//给if语句上锁
//执行到这里的时候把锁锁上
l.lock();
if(count>0){
System.out.println("准备卖票中");
//延长卖票时间(休眠)让不安全问题出现的概率增大(可省略)
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
//打印线程名字和余票,便于观察
System.out.println(Thread.currentThread().getName()+"出票成功,当前余票为:"+count);
}else{
break;
}
//执行完毕州把锁解开
l.unlock();
}
}
}
}
4、公平锁与非公平锁
公平锁:多线程按照申请锁的顺序来获取锁,不会出现争抢的情况。
非公平锁:多线程不会按照顺序获取锁,会先尝试去争抢锁,有可能后申请的线程比先申请的线程先抢到锁,在高并发情况下,可能会造成优先级反转和饥饿的现象。(例如ReentrantLock默认为非公平锁)
六、多线程通信
1、线程死锁
死锁是指多个进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象(互相挂起等待),若无外力作用,它们都将无法推进下去。
解决方法:在任何有可能导致锁产生的方法里,不要再调用另一个可能产生锁的方法
举例:绑匪和警察对话
public class Demo1 {
public static void main(String[] args) {
Culprit c = new Culprit();
Police p = new Police();
//子线程,警察说话,让罪犯回应
new MyThread(c,p).start();
//主线程,罪犯说话,让警察回应
c.say(p);
}
//建立线程
static class MyThread extends Thread {
private Culprit c;
private Police p;
public MyThread(Culprit c, Police p){
this.c = c;
this.p = p;
}
@Override
public void run() {
p.say(c);
}
}
//罪犯
static class Culprit{
public synchronized void say(Police p){
System.out.println("罪犯:你先放了我,我再放人质");
p.fun();
}
public synchronized void fun() {
System.out.println("罪犯把人质放了,警察把罪犯捉了");
}
}
//警察
static class Police{
public synchronized void say(Culprit c){
System.out.println("警察:你先把人质放了,我再放你");
c.fun();
}
public synchronized void fun() {
System.out.println("警察把罪犯放了,罪犯把人质放了");
}
}
}
2、生产者与消费者
解决方法:添加标记
举例:生产者与消费者问题(厨师与服务员)
public class Demo2 {
/**
* 多线程通信问题
* 举例:生产者与消费者问题(厨师与服务员)
*
* 解决方法:添加标记
*/
/*
//问题代码
public static void main(String[] args) {
Food f = new Food();
new Cook(f).start();
new Waiter(f).start();
}
//厨师
static class Cook extends Thread{
private Food f;
public Cook(Food f){
this.f = f;
}
@Override
public synchronized void run() {
for(int i = 0; i<100; i++){
if(i%2==0){
try {
f.setNameAndTaste("牛肉饭","美味");
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
try {
f.setNameAndTaste("炒河粉","一般般");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
//服务员
static class Waiter extends Thread{
private Food f;
public Waiter(Food f){
this.f = f;
}
@Override
public synchronized void run() {
for(int i=0; i<100; i++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get();
}
}
}
//食物
static class Food{
private String name;
private String taste;
//设置名称和味道,并在两者之间进行一个延时(休眠0.1s)以便更容易出现问题(抢到的时间片不足以进行设置味道)
public void setNameAndTaste(String name,String taste) throws InterruptedException {
this.name = name;
Thread.sleep(100);
this.taste = taste;
}
public void get(){
System.out.println("服务员端走的菜名称是:"+name+"味道是:"+taste);
}
}
*/
//解决方法:
public static void main(String[] args) {
Food f = new Food();
new Cook(f).start();
new Waiter(f).start();
}
//厨师
static class Cook extends Thread{
private Food f;
public Cook(Food f){
this.f = f;
}
@Override
public synchronized void run() {
for(int i = 0; i<100; i++){
if(i%2==0){
try {
f.setNameAndTaste("牛肉饭","美味");
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
try {
f.setNameAndTaste("炒河粉","一般般");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
//服务员
static class Waiter extends Thread{
private Food f;
public Waiter(Food f){
this.f = f;
}
@Override
public synchronized void run() {
for(int i=0; i<100; i++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
f.get();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//食物
static class Food{
private String name;
private String taste;
//true表示可以进行生产(做菜)
boolean flag = true;
//设置名称和味道,并在两者之间进行一个延时(休眠0.1s)以便更容易出现问题(抢到的时间片不足以进行设置味道)
public void setNameAndTaste(String name,String taste) throws InterruptedException {
if(flag){
this.name = name;
Thread.sleep(100);
this.taste = taste;
//做完一道菜,把标记改为false,这样就不能连续做菜了
flag = false;
//做完一道菜,唤醒所有睡眠的线程
this.notifyAll();
//厨师线程进入睡眠
this.wait();
}
}
public void get() throws InterruptedException {
//flag标记为false时表示厨师线程已休眠,此时服务员线程被唤醒,可以进行端菜
if(!flag){
System.out.println("服务员端走的菜名称是:"+name+",味道是:"+taste);
//端完菜之后flag变为true,厨师线程可以启动
flag = true;
//唤醒厨师
this.notifyAll();
//自己进入睡眠
this.wait();
}
}
}
}
七、线程的六种状态
java.lang.Thread.State枚举类中定义了六种线程的状态,可以调用线程Thread中的getState()方法获取当前线程的状态。
1、NEW
新建状态,即用new关键字新建一个线程,还未调用start方法进行启动的状态。
2、RUNNABLE
运行状态,操作系统中的就绪(ready,调用start方法后等待运行)和运行(running)两种状态,在Java中统称为RUNNABLE。
3、BLOCKED
阻塞状态,表示线程正等待监视器锁而陷入的状态。
4、WAITING
等待状态的线程正在等待另一线程执行特定的操作(如notify)
5、TIMED_WAITING
具有指定等待时间的等待状态
6、TERMINATED
线程完成执行,终止状态
7、线程状态转化
下图源自《Java并发编程艺术》图4-1
八、线程池
线程池Executors是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。 可以理解为用于存放线程的容器。
线程池的优点:
- 降低资源消耗。
- 提高响应速度。
- 提高线程的可管理性。
1、缓存线程池
public class Demo5 {
public static void main(String[] args) {
/**
缓存线程池
(长度无限制)
任务加入后的执行流程
1判断线程池是否存在空闲线程
2存在则使用
3不存在则创建线程并使用
*/
//向线程池中加入新的任务
ExecutorService service = Executors.newCachedThreadPool();
//指挥线程池执行新的任务
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"啦啦啦");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"嘻嘻嘻");
}
});service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"啦啦啦");
}
});
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2、定长线程池
public class Demo6 {
/*定长线程池
长度是指定的线程池
加入任务后的执行流程
1 判断线程池是否存在空闲线程
2 存在则使用
3 不存在空闲线程 且线程池未满的情况下 则创建线程 并放入线程池中 然后使用
4 不存在空闲线程 且线程池已满的情况下 则等待线程池的空闲线程
**/
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"啦啦啦");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"嘻嘻嘻");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"哈哈哈");
}
});
}
}
3、单线线程池
public class Demo7 {
/*单线程线程池
执行流程
1 判断线程池的那个线程是否空闲
2 空闲则使用
3 不空闲则等待它空闲后再使用
**/
public static void main(String[] args) {
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"啦啦啦");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"嘻嘻嘻");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"哈哈哈");
}
});
}
}
4、周期定长线程池
public class Demo8 {
/*周期任务 定长线程池
执行流程
1 判断线程池是否存在空闲线程
2 存在则使用
3 不存在空闲线程 且线程池未满的情况下 则创建线程 并放入线程池中 然后使用
4 不存在空闲线程 且线程池已满的情况下 则等待线程池的空闲线程
周期性任务执行时
定时执行 当某个任务触发时 自动执行某任务
**/
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
//定时执行一次
//参数1:定时执行的任务
//参数2:时长数字
//参数3:时长数字的时间单位 Timeunit的常量指定
/* scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"啦啦啦");
}
},5, TimeUnit.SECONDS); //5秒钟后执行*/
/*
周期性执行任务
参数1:任务
参数2:延迟时长数字(第一次在执行在什么时间以后)
参数3:周期时长数字(每隔多久执行一次)
参数4:时长数字的单位
* **/
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
},5,1,TimeUnit.SECONDS);
}
}