第十章-线程
一、程序-进程-线程
- 程序(program):是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态代码
- 进程(process):就是正在执行的程序,从Windows角度讲,进程就是含有内存和资源并安置线程的地方
- 线程(thread):进程可进一步细化为线程,是一个进程内部的最小执行单元(执行任务)
- 进程与程序的联系和分别
- 进程是一个动态的实体,它有自己的生命周期,反映了一个程序在一定的数据集上的运行的全部动态过程
- 一个进程肯定有一个与之相对应的程序,而且只有一个,而一个程序有可能没有与之相对应的进程(它没有执行),也有可能有多个进程与之对应(运行在几个不同的数据集上)
- 进程还具备并发性和交往性,这也与程序的封闭性不同
- 进程与线程的关系
- 进程是具有一个独立功能的程序关于某个数据集合的一次运行活动,进程是系统进行资源分配和调度的一个独立单位
- **线程是进程的一个实体,是CPU调度和分配的基本单位。**它是比进程更小的能独立运行的基本单位
- 一个进程可以包含多个线程,一个线程只能属于一个进程,线程不能脱离进程而独立运行
- 每一个进程至少包含一个线程(主线程);在主线程中开始执行程序,java程序的入口main()方法就是在主线程中被执行的
- 进程与进程之间是独立的,互不干扰的
- 在主线程中可以创建并启动其他的线程
- 一个进程内的所有线程共享该进程的内存资源
- 处理机分给线程,即真正在处理机上运行的是线程
- **划分尺度:**线程更小,所以多线程程序并发性更高
- 资源分配:进程是资源分配的基本单位,同一进程内多个线程共享其资源
- 地址空间:进程拥有独立的空间地址,同一进程内多个线程共享其资源
- 处理机调度:线程是处理器调度的基本单位
- 一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行
二、多线程的概念
- 多线程之于进程的理解,可以类比多进程之于操作系统。多线程指在单个程序中可以同时运行多个不同的线程执行不同的任务
- 何时需要多线程
- 程序需要同时执行两个或多个任务
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等
- 需要一些后台运行的程序时
- 多线程的好处
- 提高程序的响应
- 提供CPU的利用率
- 改善程序结构,将复制任务分为多个线程,独立运行
- 进程间不能共享内存,但线程间共享内存很容易
- 创建线程的代价比进程小得多,使用多线程实现多任务并发效率高
- Java内置多线程功能支持,不是单纯的作为底层OS的调度方式,简化了多线程编程
- 多线程的缺点
- 线程也是程序,所以线程需要占用内存,线程越多占用的内存就越多
- 多线程需要协调和管理,所以需要CPU跟踪线程
- 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题
三、线程的创建
1.继承Thread类的方式
public class ThreadDemo2 extends Thread{
/*
将在线程中需要执行的任务写在run方法
*/
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("run:"+i);
}
}
}
public class Demo2 {
/*
main是java中的主线程
*/
public static void main(String[] args) {
//在主线程中创建并启动其他线程
ThreadDemo2 td2 = new ThreadDemo2();//创建线程
td2.start();//启动线程
//如果使用demo2.run() 就是普通的方法调用,不是启动线程
for (int i = 0; i < 100; i++) {
System.out.println("main:"+i);
}
}
}
2.实现Runnable接口方式
主要为了解决java中不允许多继承的问题
public class ThreadDemo3 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("run:"+i);
}
}
}
public class Demo3 {
public static void main(String[] args) {
//创建了一个线程要执行的任务
ThreadDemo3 td3 = new ThreadDemo3();
//创建线程,并将线程中需要执行的任务添加到线程中
Thread td = new Thread(td3);
td.start();
for (int i = 0; i < 100; i++) {
System.out.println("main:"+i);
}
}
}
实现Runnable接口的好处
- 避免了单继承的局限性
- 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源
四、Thread类
1.构造方法
构造方法 | 说明 |
---|---|
Thread() | 创建一个线程 |
Thread(String name) | 创建一个指定名称的线程 |
Thread(Runnable target) | 利用Runnable对象创建一个线程,启动时将执行该对象的Run() |
Thread(Runnable target,String name) | 利用Runnable对象创建一个线程,并指定该线程的名称 |
2.常用方法
方法原型 | 说明 |
---|---|
void start() | 启动线程 |
final void setName(String name) | 设置线程的名称 |
final String getName() | 返回线程的名称 |
final void setPriority(int newPriority) | 设置线程的优先级 |
final int getPriority() | 返回线程的优先级 |
final void join() throws InterruptedException | 等待线程终止 |
static Thread currentThread() | 返回对当前正在执行的线程对象的引用 |
static void sleep(long millis) throws InterruptedException | 让当前正在执行的线程休眠(暂停执行),休眠时间由millis(毫秒)指定 |
五、线程的优先级
- 事实上,计算机只有一个CPU,各个线程轮流获得CPU的使用权,才能执行任务
- 优先级较高的线程有更多获得CPU的机会,反之亦然
- 优先级用整数表示,取值范围由1-10,一般情况下,线程的默认优先级都是5,但是也可以通过setPriority和getPriority方法来设置或返回优先级
1.调度策略
- 抢时间片
- 抢占式:高优先级的线程抢占CPU
2.Java的调度方法
- 同优先级线程组成先进先出队列(先到先服务),使用时间片策略
- 对高优先级,使用优先调度的抢占式策略
3.Thread类有下列3个静态常量来表示优先级
- MAX_PRIORITY:取值为10,表示最高优先级
- MIN_PRIORITY:取值为1,表示最低优先级
- NORM_PRIORITY:取值为5,表示默认的优先级
public class Demo4 {
public static void main(String[] args) {
//创建了一个线程要执行的任务
ThreadDemo3 td3 = new ThreadDemo3();
//创建线程,并将线程中需要执行的任务添加到线程中
Thread td = new Thread(td3, "自定义线程1");//可以给线程命名或者使用setName()
td.setName("aaa");
td.start();
td.setPriority(1);//setPriority()方法设置线程优先级,默认是5
Thread td1 = new Thread(td3, "自定义线程2");
td1.start();
System.out.println(Thread.currentThread().getName());//main
System.out.println(td.getPriority());//1 获得线程的优先级
System.out.println(td1.getPriority());//5
System.out.println(Thread.currentThread().getPriority());//5
}
}
public class ThreadDemo3 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("run:"+i);
}
}
}
六、线程的分类
- Java中的线程分为两类:用户线程和守护线程
- 通俗的说:任何一个守护线程都是所有非守护线程的保姆
注:
- 只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作
- 只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作
- 守护线程的作用就是为其他线程的运行提供便利服务,守护线程最典型的应该就是GC(垃圾回收器)
- 用户线程和守护线程两者没有区别,唯一的不同就在于虚拟机的离开:如果,用户线程已经全部退出运行了。只剩下守护线程存在了,虚拟机也就退出了。因为没有了守护者,守护线程也就没有工作了,也就没有运行的必要了。
- 设置线程为守护线程必须在启动线程之前,否则会出现IllegalThreadStateException异常
七、线程同步
1.并发与并行
- 并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀,多个人做同一件事
- 并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事
2.多线程同步
多个线程同时读写同一份共享资源时,可能会引起冲突,所以引入线程“同步”机制,即各线程间要有先来后到
3.同步就是排队+锁
- 几个线程之间要排队,逐个对共享资源进行操作,而不是同时进行操作
- 为了保证数据在方法中被访问时的正确性,在访问时加入锁机制
确保一个时间点只有一个线程访问共享资源。可以给共享资源加一把锁,这把锁只有一个钥匙,哪个线程获得了这把钥匙,才有权访问该共享资源
八、锁
1.同步监视器
/ 使用synchronized(同步监视器)关键字同步方法或代码块
synchronized (同步监视器){
// 需要被同步的代码;
}
// synchronized还可以放在方法声明中,表示整个方法,为同步方法
public synchronized void show (String name){
// 需要被同步的代码;
}
- 同步监视器可以是任何对象的,必须唯一,保证多个线程获得同一个对象(锁)
- 同步监视器执行过程:
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问。
- 注::一个线程持有锁会导致其他所有需要此锁的下挂起:在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
2.死锁
死锁:两个或两个以上的进程或线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象。若无外力作用,它们都将无法推进下去。
产生死锁的四个原因:
- 互斥条件:一个资源每次只能被一个线程使用
- 请求与保持条件:当一个线程请求阻塞时,它会对自己已有的资源保持不放
- 不剥夺(不可抢占资源)条件:进程已获得资源,在未使用之前,不能被强制剥夺
- 循环等待条件:若干线程之间形成一种头尾相接的循环等资源关系
避免死锁:
- 让程序每次至多只能获得一个锁。当然,在多线程的环境下,这种情况通常并不现实
- 设计是考虑清除锁的顺序,尽量减少潜在的加锁交互数量
可产生死锁代码
public class DieLock extends Thread{
static Object objA = new Object();
static Object objB = new Object();
boolean flag;
public DieLock(Boolean flag){
this.flag = flag;
}
@Override
public void run() {
if (true){
synchronized (objA){
System.out.println("if objA");
synchronized (objB){
System.out.println("if objB");
}
}
}else {
synchronized (objB){
System.out.println("else objB");
synchronized (objA){
System.out.println("else objA");
}
}
}
}
}
public class demo {
public static void main(String[] args) {
DieLock dl1 = new DieLock(true);
dl1.start();
DieLock dl2 = new DieLock(false);
dl2.start();
}
}
3.Lock(锁)
- 从JDK5.0开始,Java提供了更强大的线程同步机制-通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的位置,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前要先得到Lock对象
- ReentrantLock类实现了Lock,它拥有于synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁
1)synchronized与Lock的区别
- Lock是显式锁(手动开启和关闭锁),synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁,使用Lock锁,JVM将花费较少的时间调度线程,性能更好,并且有更好的拓展性
2)Lock方法
- lock():获取锁,如果锁被使用则一直等待
- unlock():释放锁
- trylock():返回值类型为boolean,如果获取锁的时候被占用就返回false,否则返回true
- trylock(long time,TimeUnit unit):比起tryLock()就是给了一个时间期限,保证等待参数时间
- lockInterruptibly():用该锁的获得方式,如果线程在获取锁的阶段进入了等待,那么可以中断此线程,先去做别的事。
lock()必须紧邻try{}catch{}使用
ReentrantLock是Lock接口的实现
例子:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadDemo extends Thread{
static int num =10;
static Lock lock = new ReentrantLock();//创建锁对象
@Override
public void run() {
while (true) {
try {
lock.lock();
Thread.sleep(100);
if (num > 0) {
System.out.println(num);
num--;
} else {
break;
}
lock.unlock();//释放锁
} catch (InterruptedException e) {
e.printStackTrace();
lock.unlock();
}
}
}
}
/*
使用lock锁的例子
*/
public class Test {
public static void main(String[] args) {
ThreadDemo td1 = new ThreadDemo();
td1.start();
ThreadDemo td2 = new ThreadDemo();
td2.start();
}
}
九、线程通信
- 线程通信:多个线程通过消息传递实现相互牵制,相互调度,即线程间的相互作用
- 涉及方法:
- wait():执行此方法,当前线程就会进入阻塞状态,并释放同步监视器
- notify():执行此方法,唤醒被wait()的一个线程,如果有多个线程被wait(),就会唤醒优先级较高的那个
- notifyAll():执行此方法,唤醒所有被wait()的方法
- 都定义在java.lang,.Object类中
生产者消费者问题
生产者:
/**
* 生产者线程
*/
public class Productor extends Thread{
Counter c;
public Productor(Counter c) {
this.c = c;
}
@Override
public void run() {
while (true){
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
c.add();//一直生产商品
}
}
}
消费者:
/*
消费者线程
*/
public class Customer extends Thread{
Counter c;
public Customer(Counter c) {
this.c = c;
}
@Override
public void run() {
while (true){
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
c.sub();//一直在消费产品
}
}
}
柜台:
/*
柜台类,柜台中存放共享数据
*/
public class Counter {
int num=1;//商品数目
//生产商品
public synchronized void add(){
if (num==0){//当柜台没有商品
num++;
System.out.println("生产者生产了商品");
this.notify();//唤醒消费者线程,让消费者线程消费商品 this 表示同一个柜台
}else {
try {
this.wait();//让生产线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void sub(){
if (num==1){
num--;
System.out.println("消费者取走了商品");
this.notify();//唤醒生产者线程, this 表示同一个柜台
}else {
try {
this.wait();//让消费者线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试:
/*
生产者消费者问题
生产者(Productor)将产品放在柜台(Counter),而消费者(Customer)从柜台处取走产品,生
产者一次只能生产固定数量的产品(比如:1),这时柜台中不能再放产品,此时生产者应停止生产等待消费者拿走产品,
此时生产者唤醒消费者来取走产品,消费者拿走产品后,唤醒生产者,消费者开始等待.
*/
public class Test {
public static void main(String[] args) {
Counter c= new Counter(); //创建柜台对象,是生产者和消费者的共享数据
Productor p = new Productor(c);
Customer c1= new Customer(c);
p.start();
c1.start();
}
}
十、新增创建线程方式
1.新增方式一:实现Callable接口
- 相比run(),可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask类,获取返回结果
import java.util.concurrent.Callable;
/*
求0-100的和
*/
public class SumNumThread implements Callable<Integer> {
/*
可以向外界返回结果
可以抛出异常
*/
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <=100; i++) {
sum+=i;
}
return sum;
}
}
测试:
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/*
新增两种创建线程的方式1
使用Callable接口
*/
public class Test {
public static void main(String[] args) {
//创建任务
SumNumThread s1= new SumNumThread();
//接收任务
FutureTask<Integer> futureTask =new FutureTask<>(s1);
//创建线程
Thread t1 = new Thread(futureTask);
t1.start();//启动线程
try {
Integer val =futureTask.get();//获得线程call方法的返回值
System.out.println(val);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
2.新增方式二:使用线程池
- 思路:提取创建多个线程,放入线程池中,使用时直接获取,使用完放回线程池中,可以避免频繁创建销毁,实现重复利用
- 线程池接口:ExecutorService和Executors
- 常用实现类:ThreadPoolExecutor
- 线程池工作步骤:
- 创建线程池对象
- 像线程池提交任务
- 关闭线程池
代码:
/*
新增创建线程的方式2
使用线程池
*/
import java.util.concurrent.*;
public class Test {
public static void main(String[] args) {
//在线程池中创建5个线程,等待任务执行
ExecutorService executorService = new ForkJoinPool(5);
//executors.execute();执行任务,一般为Runnable
SumNumThread1 sumNumThread1 =new SumNumThread1();
//将任务交给线程池中的线程执行
executorService.submit(sumNumThread1);
executorService.submit(sumNumThread1);
executorService.submit(sumNumThread1);
executorService.submit(sumNumThread1);
executorService.submit(sumNumThread1);
Future<Integer> f =executorService.submit(sumNumThread1);
try {
System.out.println(f.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
//关闭线程池
executorService.shutdown();
}
}