1、多线程技术概述
线程与进程:
进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间
线程:是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行. 一个进程最少有一个线程
线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程
线程调度
分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核新而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的 使用率更高。
2、同步与异步
同步:排队执行 , 效率低但是安全.
异步:同时执行 , 效率高但是数据不安全.
第一种实现多线程的方式,继承Thread类:
package com.java;
public class Demo1 {
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
for (int i=0;i<10;i++){
System.out.println("天天"+i);
}
}
}
package com.java;
public class MyThread extends Thread {
/**
* run方法就是线程要执行的方法
*/
@Override
public void run() {
//这里的代码 就是一条新的执行路径.
//这个执行路径的处罚方式,不是调用run方法.而是通过thread对象的start()来启动任务
for (int i=0;i<10;i++){
System.out.println("好好学习"+i);
}
}
}
好好学习0
好好学习1
好好学习2
好好学习3
好好学习4
好好学习5
好好学习6
好好学习7
天天0
好好学习8
天天1
好好学习9
天天2
天天3
天天4
天天5
天天6
天天7
天天8
天天9
每个线程都拥有自己的栈空间,共用一份堆内存.例如
第二种实现多线程的方式:runable
package thread;
/*
实现Runnable与继承Thread相比有如下优势
1.通过创建任务,然后给线程分配任务的方式实现多线程,更适合多个线程同时执行任务的情况
2,可以避免单继承所带来的局限性
3,任务与线程是分离的,提高了程序的健壮性
4,后期学习的线程池技术,接受Runnable类型的任务,不接受Thread类型的线程
**/
public class Demo1 {
public static void main(String[] args) {
//实现runnable
//1 创建一个任务对象
MyRunnable r = new MyRunnable();
//创建一个线程并给他一个任务
Thread t = new Thread(r);
//启动线程
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("汗滴禾下土"+i);
}
}
}
实现Runnable与继承Thread相比有如下优势
1.通过创建任务,然后给线程分配任务的方式实现多线程,更适合多个线程同时执行任务的情况
2,可以避免单继承所带来的局限性
3,任务与线程是分离的,提高了程序的健壮性
4,后期学习的线程池技术,接受Runnable类型的任务,不接受Thread类型的线程
使用匿名类的方式重新修改继承Thread的多线程实例:
public class Demo2 {
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("12345"+i);
}
}
}.start();
for (int i = 0; i < 10; i++) {
System.out.println("汗滴禾下土"+i);
}
}
}
3、获取线程的名称
package thread;
public class Demo3 {
public static void main(String[] args) {
//如何获取线程的名称
System.out.println(Thread.currentThread().getName());
//两种设置线程名称的方式
Thread t = new Thread(new MyRunnable());
t.setName("wwww");
t.start();
new Thread(new MyRunnable(),"锄禾日当午").start();
//不设置的有默认的名字
new Thread(new MyRunnable()).start();
}
static class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
}
指定时间进行线程的休眠:
//线程的休眠
for (int i = 0; i < 10; i++) {
System.out.println(i);
Thread.sleep(1000); //1000毫秒
}
线程阻塞就是比较耗时的操作,比如在控制台等待读取用户的输入和文件的读取操作等。
4、线程的中断
由于某个线程在执行任务期间,如果程序员贸然对其进行中断操作,可能由于异常中断而会导致没有释放资源,占用的这些资源就无法使用,且无法被GC回收,因此我们要想对一个线程执行中断的操作,我们可以给他一个线程中断的标记,而当某一个时刻,该线程会看他自己的标记,如果有,就自己进行中断。
在main线程执行到i=5时,给一个中断,抛出一个异常,然后对其捕获,进行继续或者中断去释放资源。
也就是说,一个线程是一个独立的执行路径,它是否结束应该由其自身决定。
public class Demo5 {
public static void main(String[] args) {
//线程中断
//y一个线程是一个独立的执行路径,它是否结束应该由其自身决定
Thread t1 = new Thread(new MyRunnable());
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添加中断标记
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) {
//e.printStackTrace();
System.out.println("发现了中断标记,线程自杀");
return;//其后可以释放资源
}
}
}
}
}
5、守护线程
线程分为守护线程和用户线程。
用户线程:当一个进程不包含任何的存活的用户线程时,进行结束
守护线程:守护用户线程的,当最后一个用户线程结束时,所有守护线程自动死亡。
在此例中,当主线程运行到i=5时,不管守护线程有没有运行完,都会伴随者主线程的死亡而死亡。
package thread;
public class Demo6 {
public static void main(String[] args) {
//线程分为守护线程和用户线程
//用户线程:当一个进程不包含任何的存活的用户线程时,进行结束
//守护线程:守护用户线程的,当最后一个用户线程结束时,所有守护线程自动死亡。
Thread t1 = new Thread(new MyRunnable());
//设置守护线程
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();
}
}
}
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) {
e.printStackTrace();
}
}
}
}
}
6、线程安全问题
为了解释线程安全的问题,设置了一个买票的案例,代码如下:
package com.java;
public class Demo7 {
public static void main(String[] args) {
//线程不安全
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
@Override
public void run() {
while (count>0){
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);//休眠来放大出问题的概率
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("卖票结束,余票:"+count);
}
}
}
}
三个线程走到最后只有一张票时,三个线程同时进入判断票数大于零,进入卖票界面,很容易争抢,最后导致卖票数小于零,导致出现了线程不安全问题。
解决方案1:同步代码块
package com.java;
//线程同步synchronized
public class Demo8 {
public static void main(String[] args) {
Object o = new Object();
//格式:synchronized(锁对象){
//
//
// }
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
private Object o = new Object();
@Override
public void run() {
//Object o = new Object(); //这里不是同一把锁,所以锁不住
while (true) {
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;
}
}
}
}
}
}
在排队锁这一概念中,锁的对象必须时同一把锁,不能是一人拿一把锁,在本例中,锁的对象必须是线程run以外定义的一个对象,而不能是在run方法中,这样的话,三个线程就同时用一把锁。
解决方案2:同步方法
我们亦可以将售票过程抽象成一个方法,然后对其锁操作。在这个方法被执行时,其他的线程等待执行该方法。
package thread;
//线程同步synchronized
public class Demo9 {
public static void main(String[] args) {
Object o = new Object();
//线程不安全
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
@Override
public void run() {
while (true) {
boolean flag = sale();
if(!flag){
break;
}
}
}
//synchronized 锁的是this,指的是该方法。
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
package thread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//同步代码块和同步方法都属于隐式锁
//线程同步lock
public class Demo10 {
public static void main(String[] args) {
Object o = new Object();
//线程不安全
//解决方案3 显示锁 Lock 子类 ReentrantLock
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
//总票数
private int count = 10;
//参数为true表示公平锁 默认是false 不是公平锁
private Lock l = new ReentrantLock(true);
@Override
public void run() {
while (true) {
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();
}
}
}
}
显式锁与隐式锁
>公平锁与非公平锁
公平锁:先来先到
非公平锁:大家一块抢,谁抢着谁运行
在使用lock时,ReentrantLock的方法中的参数为true表示公平锁 默认是false 不是公平锁
private Lock l = new ReentrantLock(true);
7、线程死锁
在开发时要尽量避免线程死锁,在使用一个方法时,该方法锁住,就要尽量避免调用另一个锁住的方法。
package thread;
public class Demo11 {
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;
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("警察救了人质,但是罪犯跑了");
}
}
}
一般会处于以下的僵持阶段,即线程死锁,结果为:
罪犯:你放了我,我放了人质
警察:你放了人质,我放了你
如果一个线程执行完了,而另一个线程还没开始执行,则会出现以下的结果:
罪犯:你放了我,我放了人质
警察救了人质,但是罪犯跑了
警察:你放了人质,我放了你
罪犯被放了,罪犯也放了人质
8、多线程通信问题
用生产者与消费者的案例来描述这个问题:
package thread;
public class Demo12 {
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 void run() {
for (int i = 0; i < 100; i++) {
if(i%2==0){
f.setNameAndTaste("老干妈小米粥","香辣味");
}else {
f.setNameAndTaste("煎饼果子","甜辣味");
}
}
}
}
//服务员
static class Waiter extends Thread{
private Food f;
public Waiter(Food f) {
this.f = f;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get();
}
}
}
//食物
static class Food{
private String name;
private String taste;
//true表示可以生产
boolean flag = true;
public synchronized void setNameAndTaste(String name,String taste){
if(flag){
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
flag = false;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void get(){
if(!flag){
System.out.println("服务员端走的菜的名称是:"+name+",味道是:"+taste);
flag = true;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
9、带返回值的线程Callable
线程除了继承Thread’、实现runnable之外,还有第三种不常用的方式:callable,它更像是主线程指派给了子线程一个任务,等待线程执行结束,给他一个返回值。在具体的程序中,我们会使用get方法来实现此流程。
Callable使用步骤:
- 编写类实现Callable接口 , 实现call方法
class XXX implements Callable<T> {
@Override
public <T> call() throws Exception {
return T;
}
}
- 创建FutureTask对象 , 并传入第一步编写的Callable类对象
FutureTask<Integer> future = new FutureTask<>(callable);
- 通过Thread,启动线程
new Thread(future).start();
Runnable 与 Callable的相同点
- 都是接口
- 都可以编写多线程程序
- 都采用Thread.start()启动线程
Runnable 与 Callable的不同点
- Runnable没有返回值;Callable可以返回执行结果
- Callable接口的call()允许抛出异常;Runnable的run()不能抛出
- Callable获取返回值
- Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执
行,如果不调用不会阻塞
例子:
package com.java;
import java . util. concurrent. Callable;
import java. util. concurrent. ExecutionException;
import java . util. concurrent. FutureTask;
/**
* @author liweijie
*/
public class Demo12 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> c = new MyCallable();
FutureTask<Integer> task = new FutureTask<>(c);
new Thread(task).start();
Integer j = task.get();
System.out.println("返回值:"+task.get());
for (int i=0;i<5;i++){
Thread.sleep(100);
System.out.println(i);
}
}
static class MyCallable implements Callable{
@Override
public Object call() throws Exception {
for (int i=0;i<5;i++){
Thread.sleep(100);
System.out.println(i);
}
return 100;
}
}
}
方法task.isDone可以判断是否执行完该线程。cancel可以结束子线程。
线程的六种状态
线程状态。 线程可以处于以下状态之一:
NEW :尚未启动的线程处于此状态。
RUNNABLE :在Java虚拟机中执行的线程处于此状态。
BLOCKED :被阻塞等待监视器锁定的线程处于此状态。
WAITING :无限期等待另一个线程执行特定操作的线程处于此状态。
TIMED_WAITING :正在等待另一个线程执行最多指定等待时间的操作的线程处于此状态。
TERMINATED :已退出的线程处于此状态。
10、线程池
线程池 Executors:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间. 线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
线程池的好处:
- 降低资源消耗。
- 提高响应速度。
- 提高线程的可管理性
线程池执行过程:
- 创建线程
- 创建任务
- 执行任务
- 关闭线程
10.1、缓存线程池(长度无限制)
任务加入后的执行流程:
- 判断线程池是否存在空闲线程
- 存在则使用
- 不存在,则创建线程 并放入线程池, 然后使用
package com.java;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo13 {
public static void main(String[] args) {
//向线程池中加入新的任务
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();
}service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
});
}
}
pool-1-thread-2锄禾日当午
pool-1-thread-3锄禾日当午
pool-1-thread-1锄禾日当午
延时之后的:
pool-1-thread-1锄禾日当午
10.2、定长线程池 (长度是指定的数值)
执行流程:
- 判断线程池是否存在空闲线程
- 存在则使用
- 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
- 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
package thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo14 {
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(3000);
} 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()+"锄禾日当午");
}
});
}
}
pool-1-thread-1锄禾日当午
pool-1-thread-2锄禾日当午
延时之后:
pool-1-thread-2锄禾日当午
10.3、单线程线程池
执行流程:
- 判断线程池的那个线程是否空闲
- 空闲则使用
- 不空闲则等待它空闲后再使用
package thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo15 {
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()+"锄禾日当午");
}
});
}
}
pool-1-thread-1锄禾日当午
pool-1-thread-1锄禾日当午
pool-1-thread-1锄禾日当午
10.4、周期性任务定长线程池
执行流程
1 判断线程池是否存在空闲线程
2 存在则使用
3 不存在空闲线程 且线程池未满的情况下 则创建线程 并放入线程池中 然后使用
4 不存在空闲线程 且线程池已满的情况下 则等待线程池的空闲线程
周期性任务执行时
定时执行 当某个任务触发时 自动执行某任务
1、定时执行一次
参数1:定时执行的任务
参数2:时长数字
参数3:2的时间单位 Timeunit的常量指定
package thread;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Demo16 {
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
},5, TimeUnit.SECONDS); //5秒钟后执行*/
}
}
2、周期性执行任务
参数1:任务
参数2:延迟时长数字(第一次在执行上面时间以后)
参数3:周期时长数字(没隔多久执行一次)
参数4:时长数字的单位
package thread;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Demo16 {
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"锄禾日当午");
}
},5,1,TimeUnit.SECONDS);
}
}