文章目录
多线程
一、基本概念
1、任务
任务是一个逻辑概念, 指由一个软件完成的活动, 或者是一系列共同达到某一目的的操作。通常一个任务是一个程序的一次运行,一个任务包含一个或多个完成独立功能的子任务,这个独立的子任务就是进程或是线程。
2、多任务
多任务处理是指用户可以在同一时间内运行多个应用程序,每个应用程序被称作一个任务。多任务系统中有3个功能单位:任务、进程和线程。
3、程序
程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念
4、进程
程序的一次执行的过程,是一个动态的概念。是操作系统资源分配的基本单位
5、线程
线程是处理器任务调度和执行的基本单位
6、进程与线程的区别
进程和线程的根本区别是进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。另外区别还有资源开销、包含关系、内存分配、影响关系、执行过程等。
**资源开销:**每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小(每个线程都有自己的工作内存)。
**包含关系:**如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
**内存分配:**同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。
**影响关系:**一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
**执行过程:**每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。
7、总内存和工作内存
–第7点转载自CSDN
Java内存模型将内存分为了 主内存和工作内存 。类的状态,也就是类之间共享的变量,是存储在主内存中的,每个线程都有一个自己的工作内存(相当于CPU高级缓冲区,这么做的目的还是在于进一步缩小存储系统与CPU之间速度的差异,提高性能),每次Java线程用到这些主内存中的变量的时候,会读一次主内存中的变量,并让这些内存在自己的工作内存中有一份拷贝,运行自己线程代码的时候,用到这些变量,操作的都是自己工作内存中的那一份。在线程代码执行完毕之后,然后在某个时间点上再将最新的值更新到主内存中去。
这样导致的问题是,如果线程1对某个变量进行了修改,线程2却有可能看不到线程1对共享变量所做的修改。
public class TestMain3{
public static void main(String[] args){
A a = new A();
new Thread(a, "a").start();
new Thread(a, "b").start();
}
}
class A implements Runnable {
static int number = 1;
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "判断时" + number);
while (number > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "要减时" + number);
number--;
System.out.println(Thread.currentThread().getName() + "减完后" + number);
}
}
}
a判断时1
b判断时1
a要减时1
b要减时1
a减完后0
b减完后-1
怎么解释??????
二、线程实现(重点)
1、继承Thread类
- 一个类继承Thread类
- 重写run()方法
- new一个新类后,运行start()方法即可开启线程,线程不一定立即执行。(如果运行run()方法,就和普通方法一样)
public class TestMain3 extends Thread{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("=====子线程中的i=" + i);
}
}
public static void main(String[] args){
TestMain3 testMain3 = new TestMain3();
testMain3.start();
for (int i = 0; i < 1000; i++) {
System.out.println("主线程中的i=" + i);
}
}
}
2、实现Runable接口
比起继承方式,更推荐使用接口方式,因为继承有局限性,不能继承其它类了。
方便一个对象被多个线程使用,而继承方式只能启用一次start。
- 一个类实现Runable接口
- 重写(实现)run()方法
- new一个类,new一个线程对象,以该类为构造参数**(静态代理)**
- 启动线程对象的start()方法(启动run()方法和普通方法调用一样)
public class TestMain3 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("=====子线程中的i=" + i);
}
}
public static void main(String[] args){
TestMain3 testMain3 = new TestMain3();
Thread thread = new Thread(testMain3);
thread.start();
for (int i = 0; i < 1000; i++) {
System.out.println("主线程中的i=" + i);
}
}
}
3、实现Callable接口
call调用,callable可调用的
public class TestMain3{
public static void main(String[] args) throws InterruptedException, ExecutionException {
A a = new A();
//创建执行服务
ExecutorService executorService = Executors.newFixedThreadPool(2);
//把实现callable接口的对象丢入执行
Future<Integer> submit = executorService.submit(a);
//得到返回值
Integer integer = submit.get();
//关闭服务
executorService.shutdown();
}
}
class A implements Callable<Integer>{
@Override
public Integer call() throws Exception {//重写call方法,返回值和接口泛型保持一致
System.out.println("执行了call方法");
return 123;
}
}
三、线程状态
1、线程五大状态
- 创建(新生)状态 创建线程对象就进入到新生状态
- 就绪状态 调用start()方法线程进入就绪状态,等待CPU调度
- 运行状态 cup调度,线程执行线程体的代码块
- 阻塞状态 当调用sleep,wait或同步锁定时,线程进入阻塞状态,代码不能往下执行,阻塞事件解除后,重新进入就绪状态
- 死亡状态 线程中断或者结束,一旦进入死亡状态,就不能再次启动
2、线程停止
- 不使用stop()、destroy()等废弃的方法
- 推荐让线程自己停止下来
- 设置一个标志位,线程中定义线程体要使用的标志位,在线程体中应用标志位,对外提供设置标志位的方法
public class TestMain3 implements Runnable{
private boolean flag = true;
@Override
public void run() {
while (flag) {
System.out.println("子线程在运行");
}
}
public void stop() {
this.flag = false;
}
public static void main(String[] args){
TestMain3 testMain3 = new TestMain3();
new Thread(testMain3).start();
for (int i = 0; i < 1000; i++) {
if (i == 999) {
System.out.println("i==" + i + "停止子线程");
testMain3.stop();
}
}
}
}
3、线程休眠
可以扩大并发问题发生性。
- Thread.sleep(毫秒数)指定当前线程阻塞的毫秒数
- sleep存在异常InterruptedException
- sleep时间达到后线程进入就绪状态
- sleep可以模拟网络延时,倒计时等
- 每一个对象都有一个锁,sleep不会释放锁,即抱着锁睡觉
- 可以在项目代码中加入sleep语句,使得某个代码运行缓慢,客户给钱就优化,并得到明显的速度提升(狗头保命)
4、线程礼让
yield /jiːld/
v. 出产(产品或作物);产出(效果、收益等);生息;屈服;放弃;停止争论;给(车辆)让路;(在外力、重压等下)屈曲
n. 产量;利润,红利率
方法:Thread.yield()
-
礼让线程,让当前正在执行的线程暂停,但不阻塞
(线程暂停:立即退出当前线程,进入就绪状态。
线程阻塞:“卡”在当前线程某一行代码,阻塞完毕后退出当前线程,进入就绪状态,如sleep方法)
-
将线程从运行状态转为就绪状态
-
让CPU重新调度,礼让不一定成功。礼让只是让当前线程重新进入竞争就绪的状态,如果cpu依然调度原线程,则称礼让不成功。
5、线程强制执行
方法:线程对象.join()
- join合并线程,等此线程执行完成后,再执行其它线程,其他线程阻塞
- 即插队
- **注意:**谁调用的就插谁的队,比如程序有100个线程,A线程中调用了B线程的join方法,那么要等到B线程执行完毕后,A线程才会继续执行,并且,在B线程执行的期间,其它线程仍然可以和B交替。
public class TestMain3 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(Thread.currentThread().getName() + "线程在运行=" + i);
}
}
public static void main(String[] args) throws InterruptedException {
TestMain3 testMain3 = new TestMain3();
new Thread(testMain3, "A").start();
new Thread(testMain3, "B").start();
new Thread(testMain3, "C").start();
for (int i = 0; i < 1000; i++) {
if (i == 888) {
Thread thread = new Thread(testMain3, "VIP!!");
thread.start();
thread.join();
}
System.out.println(Thread.currentThread().getName() + "(主)线程在运行" + i);
}
}
}
6、监测线程状态
Thread.State(枚举类型),用的时候应该用具体的线程对象来调用。
线程对象.getState();
(下面来自JDK1.8文档)
- NEW 尚未启动的线程处于此状态
- RUNNABLE 在Java虚拟机中执行的线程处于此状态,即启动start()方法后
- BLOCKED 被阻塞等待监视器锁定的线程处于此状态
- WAITING 正在等待另一个线程执行特定动作的线程处于此状态
- TIMED_WAITING 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
- TERMINATED 已退出的线程处于此状态
7、线程优先级
-
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行
-
线程的优先级用数字表示,范围从1到10
-
常量:
Thread.MIN_PRIORITY = 1
Thread.NORM_PRIORITY = 5
Thread.MAX_PRIORITY = 10
-
方法:线程对象.getPriority() 线程对象.setPriority(int)
-
设置优先级要在启动前。
-
优先级只是反映获得cup调度的概率,不是决定调度顺序。(性能倒置)
8、守护线程
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕
- 守护线程例子:后台记录操作日志,监控内存,垃圾回收GC等
- 方法:线程对象.setDaemon(boolean on)
public class TestMain3{
public static void main(String[] args){
Thread threadA = new Thread(new A());
Thread threadB = new Thread(new B());
threadB.setDaemon(true);
threadA.start();
threadB.start();
}
}
class A implements Runnable {
@Override
public void run() {
for (int i = 1000; i > 0; i--) {
System.out.println(i);
}
}
}
class B implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("无敌的我是守护线程");
}
}
}
四、线程同步(重点)
1、基本概念
- 并发Concurrence:并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。并发带来的问题:同一个对象被多个线程同时操作,即多个线程共享一个资源
- 线程同步:是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等前面线程使用完毕,下一个线程再使用
- 锁机制:当一个线程获得对象的排它锁,独占资源,其他线程必须等待该线程释放锁
- 锁带来的问题:
- 一个线程持有锁会导致其他所有需要此锁的线程挂起
- 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致性能倒置
2、同步方法与同步块
同步方法
-
synchronized public void method(){}
-
synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronizied方法都必须获得调用该方法的对象的锁才执行,否则线程会阻塞。方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。
-
锁太多,锁的方法太大会影响效率
同步块
- synchrozied(Obj){}(ArrayList就是线程不安全的,可以用同步块),锁中可以含有锁,即同步块中可以再写同步块
- Obj被称为同步监视器
- Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身(实际上锁的是这个对象中所有被synchronized修饰的方法,理解成锁对象有利于分析程序),或者是class
- 同步监视器的执行过程
- 第一个线程访问,锁定同步监视器,执行其中代码
- 第二个线程访问,发现同步监视器被锁定,无法访问
- 第一个线程访问完毕,解锁同步监视器
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问
3、死锁deadlock
-
多个线程各自占有一些共享资源,并且互相等待其它线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止的情形。某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。
-
简记:双方都强占着对方需要的资源,形成僵持
-
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
只要破解其中任意一个或多个条件就可以避免死锁发生。
public class TestMain3{
public static void main(String[] args) throws InterruptedException {
C c = new C(new A(), new B());
new Thread(c, "test1").start();
new Thread(c, "test2").start();
}
}
class A {}
class B {}
class C implements Runnable{
A a;
B b;
public C(A a, B b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
if (Thread.currentThread().getName().equals("test1")) {
synchronized (a) {
System.out.println(Thread.currentThread().getName() + "拿到了a对象的锁");
synchronized (b) {
System.out.println(Thread.currentThread().getName() + "拿到了b对象的锁");
}
}
}
if (Thread.currentThread().getName().equals("test2")) {
synchronized (b) {
System.out.println(Thread.currentThread().getName() + "拿到了b对象的锁");
synchronized (a) {
System.out.println(Thread.currentThread().getName() + "拿到了a对象的锁");
}
}
}
}//run()
}
4、Lock
- 从JKD5.0开始,Java提供了更强大的线程同步机制–通过显式定义同步锁对象来实现同步。同步锁使用Lock对象 充当
- java.util.concurrent.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
- ReentrantLock类(re + entrant)实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁
- 对于lock.lock()和lock.unlock()之间的代码块,其它线程可以访问到资源(如对象),但不能直接访问到该代码块???
与synchronized比较
- Lock是显式锁(手动开启和关闭锁),synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间一调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
- 优先使用顺序:Lock > 同步代码块(已经进入方法体,分配了相应资源) > 同步方法(在方法体之外)
代码仅作为示例,非常不规范。。。
public class TestMain3{
public static void main(String[] args) throws InterruptedException {
A a = new A();
Number1 number1 = new Number1();
a.setNumber1(number1);
new Thread(a).start();
new Thread(a).start();
System.out.println(Number1.value);//对于被锁住的代码块之间的资源,其它线程仍然可以访问
}
}
class Number1 {
static int value = 1;
}
class A implements Runnable{
Number1 number1;
private final ReentrantLock lock = new ReentrantLock();
void setNumber1(Number1 number1) {
this.number1 = number1;
}
@Override
public void run() {
while (true) {
lock.lock();
try {
if (number1.value > 0) {
Thread.sleep(10000);
number1.value--;
System.out.println(Thread.currentThread().getName() + "====" + number1.value);
} else {
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
五、线程通信(协作)问题
1、基本方法
都是Object类下的方法。下面来自JDK1.8官方文档。
需要好好理解***对象监视器(同步监视器)***的概念。
-
wait() 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法。
-
-
-
public final void wait() throws InterruptedException
导致当前线程等待,直到另一个线程调用该对象的
notify()
方法或notifyAll()
方法。换句话说,这个方法的行为就好像简单地执行呼叫wait(0)
。当前的线程必须拥有该对象的显示器。 该线程释放此监视器的所有权,并等待另一个线程通知等待该对象监视器的线程通过调用
notify
方法或notifyAll
方法notifyAll
。 然后线程等待,直到它可以重新获得监视器的所有权并恢复执行。像在一个参数版本中,中断和虚假唤醒是可能的,并且该方法应该始终在循环中使用:
synchronized (obj) { while (<condition does not hold>) obj.wait(); ... // Perform action appropriate to condition }
**该方法只能由作为该对象的监视器的所有者的线程调用。**有关线程可以成为监视器所有者的方式的说明,请参阅
notify
方法。-
异常
IllegalMonitorStateException
- 如果当前线程不是对象监视器的所有者。InterruptedException
- 如果任何线程在当前线程等待通知之前或当前线程中断当前线程。 当抛出此异常时,当前线程的中断状态将被清除。
-
-
-
-
-
notify() 唤醒正在等待对象监视器的单个线程。
public final void notify()唤醒正在等待对象监视器的单个线程。 如果任何线程正在等待这个对象,其中一个被选择被唤醒。 选择是任意的,并且由实施的判断发生。 线程通过调用wait方法之一等待对象的监视器。
唤醒的线程将无法继续,直到当前线程放弃此对象上的锁定为止。 唤醒的线程将以通常的方式与任何其他线程竞争,这些线程可能正在积极地竞争在该对象上进行同步; 例如,唤醒的线程在下一个锁定该对象的线程中没有可靠的权限或缺点。该方法只能由作为该对象的监视器的所有者的线程调用。 线程以三种方式之一成为对象监视器的所有者:
通过执行该对象的同步实例方法。
通过执行在对象上synchronized synchronized语句的正文。
对于类型为Class,的对象,通过执行该类的同步静态方法。
一次只能有一个线程可以拥有一个对象的显示器。异常
IllegalMonitorStateException - 如果当前线程不是此对象的监视器的所有者。 -
notifyAll() 唤醒正在等待对象监视器的所有线程。
public final void notifyAll()唤醒正在等待对象监视器的所有线程。 线程通过调用wait方法之一等待对象的监视器。
唤醒的线程将无法继续,直到当前线程释放该对象上的锁。 唤醒的线程将以通常的方式与任何其他线程竞争,这些线程可能正在积极地竞争在该对象上进行同步; 例如,唤醒的线程在下一个锁定该对象的线程中不会有可靠的特权或缺点。该方法只能由作为该对象的监视器的所有者的线程调用。 有关线程可以成为监视器所有者的方法的说明,请参阅notify方法。
异常
IllegalMonitorStateException - 如果当前线程不是此对象的监视器的所有者。
2、生产者与消费者模式(问题)
是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。
- 对于生产者,没有生产产品之前,要通知消费者等待(消费者自己启用wait方法)。而生产了产品之后,又需要马上通知消费者消费
- 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费
-
管程法
生产者 ↔ 缓冲区 ↔ 消费者
-
信号灯法:标志位
六、高级主题
1、线程池
- 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
- 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具
- 好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 全球线程管理
- 关键字:
- corePoolSize:核心池的大小
- maximunPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间会终止
- 线程池接口:ExecutorService,Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
public class TestMain3{
public static void main(String[] args) throws InterruptedException, ExecutionException {
A a = new A();
B b = new B();
//创建执行服务,创建一个大小为2的线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
//把实现callable接口的对象丢入执行
Future<Integer> submit = executorService.submit(a);
//也可以把实现Runnable接口的对象丢入执行
executorService.execute(b);
executorService.execute(b);
//实现callable接口的对象,可以得到得到返回值
Integer integer = submit.get();
//关闭服务
executorService.shutdown();
}
}
class A implements Callable<Integer>{
@Override
public Integer call() throws Exception {//重写call方法,返回值和接口泛型保持一致
System.out.println(Thread.currentThread().getName() + "执行了call方法");
return 123;
}
}
class B implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行了run方法");
}
}
后记:上面的大都是看狂神的视频做的笔记,加上自己找的一些资料和实例,感谢他的免费课程~~