多线程详细基础教程
多线程必掌握的重点知识点
- 多线程,进程,程序之间的区别
- 多线程生命周期
- 多线程常用方法
- 多线程创建方式
- 多线程的线程安全问题(数据共享,同步synchronized,Lock,锁的概念)
- 多线程死锁
- 多线程协作(消费者生产者)
- volatile关键字
- 线程池
一、多线程的基本概念
1. 线程、进程、程序之间的区别
-
定义
- 进程:运行中的程序,操作系统会分配进程给该程序
- 程序:静态的代码
- 线程:不能独立存在,线程是存在在进程中,一个进程可以有多个线程
-
区别
- 进程:是由cpu创建并分配独立的存储空间,每一个进程都有独立的存储空间
- 多线程:有独立的运行堆栈和程序计数器,但没有独立的存储空间,共享进程的存储空间
- 多线程:有独立的运行堆栈和程序计数器,但没有独立的存储空间,共享进程的存储空间
- 线程是一个轻量级的进程,必须依赖于进程的存在,线程之间的切换相对快一些
- 进程是一个重量级,有独立的存储空间,进程之间的切换就要慢一些
2. 线程的生命周期
-
Thread t = new MyThread();//创建一个线程,只是空的线程对象,并没有分配运行堆栈
-
t.start();//启动一个线程,使线程进入就绪状态,才分配独立的执行堆栈
-
run();//运行run(),就是运行状态
-
run()执行完毕,就是线程结束状态
3. 线程的状态及转换
- 新建状态 new
- 就绪状态 start(),阻塞完毕,或者调用其他线程,yield()
- 运行状态 由CPU统一管理执行,什么时候进入运行状态不受程序员的控制
- 结束状态 当run()执行完毕的时候,线程结束,或者出现异常程序退出
- 阻塞状态 sleep(),wait()等
-
新建—》就绪
-
阻塞—》就绪
-
就绪—》运行
-
运行—》就绪
-
运行—》阻塞
-
运行—》结束
二、多线程的使用
1.多线程的创建
- implements Runnable(推荐)
必须实现public void run(){}
还要结合Thread才能使用线程
public class MyThread implements Runnable{
int i = 0;
public void run() {
while(i < 100) {
System.out.println(i);
i++;
}
}
}
MyThread mt = new MyThread();//这个类实现了Runnable接口
Thread t = new Thread(mt);//定义一个线程类封装
t.start();//启动线程,使线程进入就绪状态
- extends Thread
public class MyThread2 extends Thread{
int i = 0;
public void run() {
while(i < 1000) {
System.out.println("线程2:"+i);
i++;
}
}
}
MyThread2 mt2 = new MyThread2();
mt2.start();
- Callable接口
- 优点:可以在线程执行完提供返回值FutrueTask
2.多线程的方法
- start() ;//启动线程,是线程进入就绪状态,等待CPU调度执行
- sleep(long ms);//使线程进入阻塞状态,等待指定时间后,使线程进入就绪状态
- getId();//获取线程id值
- run();//线程体,当线程执行的时候,执行该方法
- getName();//获取线程的名称,该名称可以指定,可以通过构造方法指定Thread(String name)或Thread(Runnable r,String name)
- getPriority();//获取线程的优先级,默认的优先级是5 ,范围1-10
- setPriority();//线程的优先级只是给cpu一个建议,优先级高不能保证优先执行
- isAlive();//判断线程是否是存活状态,线程结束isAlive()就是false,其他都是true
- yield();//线程正在执行,让线程让出CPU的使用,回到就绪状态
- wait();//Object的方法
- notify();//Object的方法
- Thread.currentThread().getName();//获取当前线程的名字
3.多线程的应用
- join() 加入线程
- 当一个线程A加入到另外一个线程B中,必须确保A线程执行完,才能继续执行B线程
public class MyThread extends Thread {
public MyThread(){
}
public void run(){
int i = 0;
while(i < 10){
System.out.println("B子线程执行" + i);
i++;
}
}
}
public class Test01 {
public static void main(String[] args) {
//创建子线程
Thread t = new MyThread();
//启动子线程
t.start();
/*try {
//使用该方式也可以增大主线程最后才被执行完,但:
//1.无法控制时间确保主线程100%最后执行
//2.休眠时间长可能会产生浪费时间的情况
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
try {
//把线程t加入到主线程中,确保t一定执行完后,才能继续执行主线程
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("A主线程结束......");
}
}
- setDaemon()设置守护进程
- 守护线程:当主线程结束的时候,守护线程也会跟着退出
public class MyThread2 extends Thread {
public MyThread2(){
//设置当前线程为守护线程,当主线程结束时,该线程也会退出
setDaemon(true);
}
public void run(){
while(true){
System.out.println("B子线程执行...");
}
}
}
public class Test02 {
public static void main(String[] args) {
//创建子线程
Thread t = new MyThread2();
//启动子线程
t.start();
System.out.println("A主线程结束......");
}
}
三、线程的数据共享与同步
-
吃包子:张三、李四、王五3个人共吃10个包子
-
分析:张三、李四、王五就是3个线程,如何保证一起吃10个包子
实现:
public class Baozi implements Runnable{
//加上static,这个变量就是一个共享变量,所有的线程都共享这一个变量
static int baozi = 10;
@Override
public void run() {
while (baozi > 0){
System.out.println(Thread.currentThread().getName() + "吃第" + baozi + "个包子...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
baozi--;
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new Baozi() , "张三");
Thread t2 = new Thread(new Baozi() , "李四");
Thread t3 = new Thread(new Baozi() , "王五");
t1.start();
t2.start();
t3.start();
}
}
王五吃第10个包子...
张三吃第10个包子...
李四吃第10个包子...
张三吃第8个包子...
李四吃第8个包子...
王五吃第7个包子...
王五吃第6个包子...
李四吃第4个包子...
张三吃第4个包子...
王五吃第3个包子...
李四吃第2个包子...
张三吃第1个包子...
注意:以上的输出和我们需求是不一致的,3个人同吃了一个包子,就是在数据共享的时候,产生了多线程的数据不安全性的
1.数据共享的问题
-
线程安全: StringBuffer 、Vector 、HashMap
-
线程不安全: StringBuilder、ArrayList、HashTable,当存在多线程的时候,共享数据不保证其数据的准确性
线程不安全其实就是数据的不安全性
-
分析数据不安全的原因:
- 线程执行的不确定性
- 多线程存在共享数据
- 线程交替执行
注意:当多个线程同时操作同一个数据的时候,有可能会出现数据不安全的情况,要保证数据安全,可以采用同步方式。
2.同步的概念
(1)同步和异步的区别
-
异步: 多线程交替执行,可以充分的利用cpu,提升效率,缺点可能会造成数据的不安全性
-
同步: 多线程排队执行,优点:保证数据的安全性,缺点:执行效率相对比较低
当一个线程执行完同步代码后,另外一个线程才能进入执行
同步在一些特殊情况下不一定比异步的效率低,因为异步会存在频繁的切换线程,这时就需要把先线程的状态保存以及进行恢复,这些都有可能会浪费效率,所以说同步不一定比异步效率低。
(2)锁的概念
-
锁就是应用在同步上,锁:原子性、排斥性,可见性等特点
-
同步synchronized,应用锁lock
(3)同步的应用
-
同步代码块
synchronized(锁){ //类、对象等都可以作为锁,一般的锁应该都是线程共享的锁 //需要同步的同步代码块 }
public class Baozi implements Runnable{ //加上static,这个变量就是一个共享变量,所有的线程都共享这一个变量 static int baozi = 10; static Object lock = new Object();//锁,多个线程共享这一把锁 @Override public void run() { while (baozi > 0) { //同步代码块 synchronized (lock) { //加判断,确保baozi>0,才能吃 if(baozi > 0 ) { System.out.println(Thread.currentThread().getName() + "吃第" + baozi + "个包子..."); baozi--; } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
-
同步方法锁
//代表当前方法是个同步方法,当一个线程执行该方法,其他的线程必须等待此线程的方法执行完成才能进入/该方法 //非静态同步方法,是把当前对象作为锁 public synchronized void eat(){ } //静态同步方法,是把当前类作为锁 public synchronized static void eat(){ }
3.synchronized底层原理(了解)
jdk1.6之后的原理,jdk1.6之前synchronized只是一个重量级锁,1.6以后进行了优化,效率有明显的提升
第一阶段:偏向锁,第一次由哪个线程抢到锁,锁修改一个锁的标识位(偏向锁01),当只有一个线程的时候,他会减少锁的获取,锁偏向该线程,不需要重新获取该锁,可以直接进入同步代码块执行
第二阶段:当有其他的线程参与争抢该锁时,锁就会修改标志位变成轻量级锁(自旋锁,不停的循环判断锁是否被释放),自旋锁主动的判断锁是否可以被获取,效率相对比较高,但是判断的次数是有一定限制的,当超过一定次数后,自旋就停止,进入阻塞等待状态(从主动判断锁是否被释放变成被动等待锁被释放后通知),进入第三阶段
第三阶段:重量级锁,所有的线程只有等待锁释放后,锁会通知所有线程可以重新过来抢锁
锁的膨胀(升级): 偏向锁-----》轻量级锁(自旋锁)---->重量级锁,该过程不可逆
4.可重入锁(比较常用的一种方式)Lock接口
ReentrantLock
与synchronized
区别
ReentrantLock
是手动的操作锁
synchronized
是自动的操作锁
-
创建锁:Lock lock = new ReentrantLock();
-
上锁:lock.lock();
-
释放锁: lock.unlock();//使用lock一定要执行该方法
公平锁和非公平锁区别
非公平锁: 线程的执行是不可预知的,谁能抢就执行谁
公平锁: 线程的执行是公平的,每个线程会按照次序公平均衡的执行
哪个锁好是需要看业务场景的,大多数情况为了执行效率,会采用非公平锁,假如为了保证线程均衡执行会采用公平锁
-
synchronized是非公平锁
-
ReentrantLock既可以是公平锁,也可以是非公平锁,默认是非公平锁
-
new ReentrantLock(boolean fair);//true是公平锁,false就是非公平锁,默认是false
-
可重入锁: 当线程进入,只有当前线程可以重复获取同一把锁
lock.lock();//第一次上锁
eat();
lock.unlock();//必须释放锁,一定在finally语句块
public void eat(){
lock.lock();//第二次执行的,已经在第一次获取到该锁,第二次可以直接能继续获取该锁
}
四、线程的死锁
-
只有在线程同步(锁)的时候,才有可能出现死锁。
-
死锁的产生: 当多个线程在获取锁的时候进入阻塞状态,等待其他锁的释放而进入
永久等待
public class DeadLock {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
private static class T1 extends Thread{
@Override
public void run() {
synchronized (lock1){
System.out.println("线程1获取到第一把锁......");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2){
System.out.println("线程1获取到第二把锁....");
}
}
}
}
private static class T2 extends Thread{
@Override
public void run() {
synchronized (lock2){
System.out.println("线程2获取到第二把锁......");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1){
System.out.println("线程2获取到第一把锁....");
}
}
}
}
public static void main(String[] args) {
new T1().start();
new T2().start();
}
}
避免死锁:
1. 建议不要使用嵌套的环形锁
当出现嵌套锁的时候,假如获取到锁的顺序是一致的,不会产生死锁的问题
public class DeadLock {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
private static class T1 extends Thread{
@Override
public void run() {
synchronized (lock1){
System.out.println("线程1获取到第一把锁......");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2){
System.out.println("线程1获取到第二把锁....");
}
}
}
}
private static class T2 extends Thread{
@Override
public void run() {
synchronized (lock1){
System.out.println("线程2获取到第二把锁......");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2){
System.out.println("线程2获取到第一把锁....");
}
}
}
}
public static void main(String[] args) {
new T1().start();
new T2().start();
}
}
假如有多把锁的时候,建议给锁进行排序,根据排序安排使用锁的顺序,这样所有线程就不会产生死锁的问题
2. 可以使用lock接口
lock.lock()------->lock.tryLock(int value , int timeunit)//会在单位的时候内假如没有获取到该锁就会放弃,不会一直在阻塞状态
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Baozi3 implements Runnable{
//加上static,这个变量就是一个共享变量,所有的线程都共享这一个变量
static int baozi = 10;
static Lock lock = new ReentrantLock(true);
@Override
public void run() {
while (baozi > 0) {
//手动上锁
try {
//lock.lock();
if(lock.tryLock(1, TimeUnit.SECONDS)) {
//加判断,确保baozi>0,才能吃
if (baozi > 0) {
System.out.println(Thread.currentThread().getName() + "吃第" + baozi + "个包子...");
baozi--;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//使用可重入锁,必须要手动释放锁
lock.unlock();
}
}
}
}
五、线程的协作
线程协作经典实例(生产者和消费者问题)
-
需求: 假如有2个生产者生成热狗,有3个消费者吃热狗,生产到10个热狗时停止,消费者吃光的时候不能继续消费。 消费者和生成者是不是有个协作关系,消费者负责消费,生产者负责生成,消费和生产都是同一个商品
-
Object类中有3个方法(是与线程有关系的): wait(),notify(),notifyAll(),主要用在锁对象上
-
wait();//使线程进入阻塞状态
-
notify();//通知处于wait状态(阻塞状态)的线程可以继续执行了
-
notifyAll();//通知所有处于wait状态的线程都可以继续执行了
-
import java.util.ArrayList;
import java.util.List;
/**
* 假如有2个生产者生成热狗,有3个消费者吃热狗,生产到10个热狗时停止,消费者吃光的时候不能继续消费。
* 消费者和生成者是不是有个协作关系,消费者负责消费,生产者负责生成,消费和生产都是同一个商品
*/
public class Test01{
//定义装热狗的容器,也是一把锁
private static List<Integer> hotDogs = new ArrayList<>();
//生产者线程
private static class Producer extends Thread{
int i = 1;
int pid;
public Producer(int pid){
this.pid = pid;
}
public void run(){
while(true){
synchronized (hotDogs){
if(hotDogs.size() < 10){
System.out.println(pid + "号生产者生产" + (pid*1000 + i) + "号热狗");
hotDogs.add(pid*1000 + i);
i++;
hotDogs.notifyAll();//通知消费者已经有热狗了,你不要wait了,可以继续执行吃热狗
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
System.out.println("热狗已经满了10个。。。。。,暂停生产");
try {
hotDogs.wait();//热狗已经满了,强制生产者休息...
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
//消费者线程
private static class Consumer extends Thread{
int cid;
public Consumer(int cid){
this.cid = cid;
}
public void run(){
while(true){
synchronized (hotDogs){
if(hotDogs.size() > 0){
System.out.println(cid + "消费者吃" + hotDogs.remove(0) + "号热狗");
hotDogs.notifyAll();//已经被我吃了一个,通知所有的生成者不要wait了,可以继续生成热狗
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
System.out.println("热狗数为0,已经不足,,,,,,");
try {
hotDogs.wait();//热狗没有了,强制消费者休息
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
public static void main(String[] args) {
for (int i = 1; i <= 2 ; i++) {
new Producer(i).start();
}
for (int i = 1; i <=3 ; i++) {
new Consumer(i).start();
}
}
}
注意:生成者和消费者一定是公用一把锁,才能达到通知等待的效果,否则notify和wait没有作用的
六、volatile关键字
1.作用
-
线程可见性
class RunThread extends Thread{ volatile boolean b = true;//可见性 int x = 0; public void run() { while(b) { x++; } System.out.println(x); System.out.println("线程停止。。。"); } public void setB(boolean b) { this.b = b; } }
public class Test01 {
public static void main(String[] args) {
RunThread r = new RunThread();
r.start();
try {
Thread.sleep(20);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
r.setB(false);
}
}
2. 禁止指令重排
- cpu有可能会根据相应的优化条件,会对一些语句的顺序进行调整,这种指令重排
```java
//这种指令重排在一般情况或者单线程下是没有影响的,但在一些多线程的特殊条件下有可能会出现问题。
int a = 1;
int b = 2;
int c = a + b ;//假如有依赖关系的指令不会进行重排
package ch501.c03;
public class TestVola extends Thread{
static int a = 1;
static boolean b = false;
public void change() {
a = 2;
//有可能会出现指令重排的问题,这是b = true优先a=2执行,那么在run()中有可能输出的就是1而不是2
b = true;
}
public void run() {
if(b) {
System.out.println(a);//有可能结果是1也可能是2
}
}
}
如何解决由于指令重排可能造成的结果的不确定性?
使用volatile关键字。
public class TestVola extends Thread{
volatile static int a = 1;//使用volatile禁止指令重排,a=2就一定在b=true前边执行
static boolean b = false;
public void change() {
a = 2;
//有可能会出现指令重排的问题,这是b = true优先a=2执行,那么在run()中有可能输出的就是1而不是2
b = true;
}
public void run() {
if(b) {
System.out.println(a);//有可能结果是1也可能是2
}
}
}
2.不足
- 不能保证原子性
public class TestVola2 {
volatile static int i = 0;//volatile不能保证原子性
static void change() {
i++;//不具备原子性 (1)i=0 (2)0+1=1 (3)i=1
}
public static void main(String[] args) {
for(int j = 0 ; j < 10000 ; j++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
change();
}
}).start();
}
System.out.println(i);
}
}
我们想要的 结果应该是10000,但实际的效果是<=10000,如何解决。
import java.util.concurrent.atomic.AtomicInteger;
public class TestVola2 {
//volatile static int i = 0;//volatile不能保证原子性
volatile static AtomicInteger i = new AtomicInteger(0);//原子性整数值
static void change() {
//i++;//不具备原子性 (1)i=0 (2)0+1=1 (3)i=1
i.incrementAndGet();//该操作具备原子性
}
public static void main(String[] args) {
for(int j = 0 ; j < 10000 ; j++) {
new Thread(new Runnable() {
@Override
public void run() {
change();
}
}).start();
}
/*try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}*/
while(Thread.activeCount() > 1) {
System.out.println(Thread.activeCount());
Thread.yield();
}
System.out.println(i);
}
}
七、线程池
-
线程池: 放多线程的池子。是多线程创建的一种常用方式。
-
创建线程的3种方式:
-
(1)extends Thread
-
(2)implements Runnable
-
(3)implements Callable (多线程可以有返回值)
-
上边3种的缺点:
-
(1)new Thread()新建一个对象性能比较低
-
(2)线程缺乏统一的管理,有可能会无限制的创建线程,造成线程之前互相争抢资源,造成cpu切换的浪费的资源以及内存资源的浪费,当线程过多,可能会出现OOM(内存溢出)
-
(3)功能比较单一,不能定时执行
针对以上问题,可以使用线程池来解决。
JDK提供了几种线程池的现成方案:(全是基于ThreadPoolExecutor)
-
-
-
Excutors提供的方法,返回的值是ExcutorService
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue
- newCachedThreadPool缓存线程池
corePoolSize 0
maximumPoolSize Integer.MAX_VALUE
keepAliveTime 60L
unit TimeUnit.SECONDS
workQueue SynchronousQueue
public class Test {
public static void main(String[] args) {
//缓存型线程池
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
for (int i = 1; i <= 100; i++) {
final int index = i;
newCachedThreadPool.execute(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName() + "执行第" + index + "个任务");
}
});
}
}
}
1. newFixedThreadPool固定线程池
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(5);
for (int i = 1; i <= 100; i++) {
final int index = i;
newFixedThreadPool.execute(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName() + "执行第" + index + "个任务");
}
});
}
2. newSingleThreadExecutor单线程池
```java
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 1; i <= 100; i++) {
final int index = i;
newSingleThreadExecutor.execute(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName() + "执行第" + index + "个任务");
}
});
}
```
3. newScheduledThreadPool任务型线程池
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);
/* newScheduledThreadPool.schedule(new Runnable() {
@Override
public void run() {
System.out.println("延迟3秒后执行 该输出....");
}
}, 3, TimeUnit.SECONDS);*/
newScheduledThreadPool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("1秒后每隔3秒输出该句话.....");
}
}, 1, 3, TimeUnit.SECONDS);
几种线程池的缺点:非核心线程数或者工作队列都有可能非常非常大(最大就是Integer.MAX_VALUE),可能会造成服务器的宕机。可以根据自身的需要直接使用自定义的线程池
4. 自定义线程池
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
ExecutorService es = new ThreadPoolExecutor(10,20,30,TimeUnit.SECOND,new ArrayBlockingQueue(100));
5. 线程池相关方法
-
shutdown();//关闭线程池,要保证还没执行完线程执行完任务后才关闭
-
shutdownNow();//不能保证线程已经执行完任务
-
execute();//执行线程任务,没有返回值
-
submit();//执行线程任务,有返回值