多线程(一)---初阶篇

一、初识线程

1.什么是线程

进程是系统分配资源的最小单位,线程是系统调度的最小单位,一个进程内的线程之间是可以共享资源的,每个进程至少有一个线程存在,即主线程

2.创建线程的方式

2.1创建线程-方法1-继承Thread类

可以通过继承Thread来创建一个线程类,该方法的好处是this代表的就是当前线程,不需要通过Thread.current.Thread来获取当前线程的引用

class MyThread extends Thread{
	@Override
	public void run(){
	System.out.println("这里是线程运行的代码");
	}
}
MyThread t = new MyThread();
t.start();//线程开始运行
2.2创建线程-方法2-实现Runnable接口

通过实现Runnable接口,并且调用Thread的构造方法时将Runnable对象作为target参数传入来创建对象,该方法的好处是可以规避类的单继承的限制,但需要通过Thread.currentThread()来获取当前线程的引用

class MyRunnable implements Runnable{
	@Override
	public void run(){
		System.out.println(Thread.currentThread().getName()+"这里是线程运行的代码");
	}
}
Thread t = new Thread(new MyRunnable());
t.start();//线程开始运行
2.3创建线程-其他变形(了解)
//使用匿名内部类创建Thread子类对象
Thread t1 = new Thread(){
	@Override
	public void run(){
		System.out.println("使用匿名内部类创建Thread子类对象");
	}
}
//使用匿名内部类创建Runnable子类对象
Thread t2 = new Thread(new Runnable(){
	@Override
	public void run(){
		System.out.println("使用匿名内部类创建Runnable子类对象");
	}
});
//使用lambda表达式创建Runnable子类对象
Thread t3 = new Thread(()->System.out.println("使用匿名内部类创建Thread子类对象"));
Thread t4 = new Thread(()->{
	System.out.println("使用匿名内部类创建Thread子类对象");

二、Thread类

1.什么是Thread类

Thread类是JVM用来管理线程的一个类,换句话说,每个线程都有一个唯一的Thread对象与之关联
每个执行流,也需要有一个对象来描述,类似于下图所示,而Thread类的对象就是用来描述一个线程执行流的,JVM会将这些Thread对象组织起来,用于线程调度,线程管理
在这里插入图片描述

2.Thread的常见构造方法

方法说明
Thread创建线程对象
Thread(Runnable target)使用Runnable对象创建线程对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target,String name)使用Runnable对象创建线程对象,并命名
Thread(ThreadGroup group,Runnable target)线程可以被用来分组管理,分好的组即使线程组

3.Thread的常见属性

属性获取方法
IDlong getID()
名称String getName()
状态Thread.state getState()
优先级int getPriority()
是否后台线程(守护线程)boolaean isDaemon()
是否存活boolean isAlive()
是否被中断boolean isInterruppted()
中断这个线程void interrupt ()
等待这个线程死亡void join()
等待这个线程最多mills msvoid join(long millis)
如果这个线程使用单独的Runnable运行对象构造,则调用该Runnable对象的run方法,否则不执行任何操作并返回void run()
将此线程标记为daemon线程或用户线程
设置线程名void setName(String name)
设置优先级void setPriority(int newPriority()
导致线程开始执行,Java虚拟机调用此线程的run方法void start()
  • ID是线程的唯一标识,不同线程不会重复
  • 名称是各种调试工具用到
  • 状态表示线程当前所处的一个情况
  • 优先级高的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住一个点:JVM会在一个进程的所有非后台线程结束后,才会结束运行
  • 是否存活,即简单的理解,为run方法是否运行结束了

三、多线程的效率问题

最高效率和系统资源+线程数+单个线程执行的任务量都有关系

1.使用多线程提高效率需要考虑的因素

  • 所有线程执行是并发+并行
  • 线程创建、销毁是比较耗时
  • 线程的调度由系统决定(线程越多,系统调度越频繁,线程就绪态转变为运行态也是有性能及时间消耗)
  • 单个线程运行的任务量

2.run方法 vs start方法

  • run方法是线程运行的时候执行的代码块,线程启动是通过start方法启动
  • run方法直接调用,不会启动线程,只是在当前main线程中调用run方法
  • start方法启动这个线程

3.java进程的退出

  • 至少有一个线程是非守护线程,没有被销毁,进程就不会退出
  • 非守护线程一般可以称为工作线程,守护线程可以称为后台线程
  • t.setDaemon(true)设置守护线程
    注意事项:
    优先级更高的线程,更有可能会先执行,但不是一定,知识几率更大

    Runnable是就绪态和运行态的并集

四、多线程

1.线程的让步

Thread.yield;//将当前线程由运行态转变为就绪态

2.线程的等待

t.join();
t.join(2000);

3.休眠当前线程

sleep.sleep(8000);

4.线程的中断

不是真实的直接中断,具体是否要中断,由线程自己决定

boolean isInterrupted();//测试这个线程是否被中断
void interrupt();//中断这个线程
static boolean interrupted();//测试这个线程是否中断

  • 线程调用wait()/join()/sleep()阻塞时,如果把当前线程给中断,会直接抛一个异常,而线程运行状态时,需要自行判断中断标志位,处理中断操作,阻塞状态时,通过捕获及处理异常来处理中断线程的逻辑
  • 抛出异常后,线程中断标志位会重置
  • 线程的真实的中断方法:过期方法stop()
  • 线程启动以后,中断标志位为false
  • 在线程运行态中,处理线程中断,需要自行通过判断中断标志位,来进行中断的处理逻辑(Thread.isInterrupted()/Thread.interrupted()),通过这两种方法来判断
  • 线程因调用wait()/join()/sleep()处于阻塞状态时,将线程中断,会直接抛出Interrupted Exception异常
  • 抛出异常后,重置线程的中断标志位为true
  • 也可以使用自定义的中断标志位
  • 自定义的标志位能满足线程处于运行态的中断操作,但不能满足线程处于阻塞状态时的中断操作

5.通信-对象的等待集wait set

  • wait()的作用是让当前线程进入等待作用,同时,wait()也会让当前对象释放它所持有的的锁,“直到其他线程调用此对象的notify()方法或notifyAll()方法”,当前线程被唤醒(进入“就绪状态”)
  • notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程,notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程
  • wait(long timeout)让当前线程处于(阻塞)状态,直到其他线程调用此对象的额notify()方法或者notifyAll()方法,或者超过指定的时间量,当前线程被唤醒(进入“就绪状态”)
5.1wait方法()

wait()方法就是使线程停止运行

  • 方法wait()的作用是使当前执行代码的线程进行等待,wait()方法是Object类的方法,该方法是用来将当前线程置入“预执行队列”中,并且在wait()所在的代码处停止执行,直到接到通知或被中断为止
  • wait()方法只能在同步方法中或同步块中调用,如果调用wait()时,没有持有适当的锁,会抛出异常
  • wait()方法执行后,当前线程释放锁,线程与其他线程竞争重新获取锁
public static void main(String[] args) throws InterruptedException {
	Object object = new Object();
	synchronized (object) {
		System.out.println("等待中...");
		object.wait();
		System.out.println("等待已过...");
	}
	System.out.println("main方法结束...");
}

这样在执行到object.wait()之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就需要使用到了另外一个方法唤醒的方法notify()

5.2notify()方法

notify方法就是使停止的线程继续运行。

  1. 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。如果有多个线程等待,则有线程规划器随机挑选出一个呈wait状态的线程。
  2. 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出
    同步代码块之后才会释放对象锁。
5.3 notifyAll()方法

notifyAll方法可以一次唤醒所有的等待线程
注意:唤醒线程不能过早,如果在还没有线程在等待中时,过早的唤醒线程,这个时候就会出现先唤醒,在等待的效果了。这样就没有必要在去运行wait方法了。

5.4 wait 和 sleep 的对比(面试题)

其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间。用生活中的例子说的话就是婚礼时会吃糖,和家里自己吃糖之间有差别。说白了放弃线程执行只是 wait 的一小段现象。
当然为了面试的目的,我们还是总结下:

  1. wait 之前需要请求锁,而wait执行时会先释放锁,等被唤醒时再重新请求锁。这个锁是 wait 对像上的 monitor lock
  2. sleep 是无视锁的存在的,即之前请求的锁不会释放,没有锁也不会请求。
  3. wait 是 Object 的方法
  4. sleep 是 Thread 的静态方法

五、线程安全问题

1.线程不安全的操作

private static final int NUM = 20;
private static final int COUNT = 1000;
//同时启动20个线程,每个线程对同一个变量操作,循环1000次,循环++操作
private static int SUM;//int 数据类型,值处于-128-127,在常量池中,超出范围,处于堆
public static void main(String[] args){
	for(int i = 0;i < NUM;i++){
		new Thread(new Runnable(){
			@Override
			public void run(){
				for(int j = 0;j < COUNT;j++){
					SUM++;
				}
			}
		}).start();
		while(Thread.activeCount()>1){
			System.out.println();
		}
}
//打印结果每次不同且不是200000

2.线程的安全问题

a. 原子性

  • List item不具有原子性,在代码行之间插入了并发/并行执行的其他代码
  • 造成的结果:业务逻辑处理出现问题
  • 特殊的原子性代码:(分解执行存在编译为class文件时,也可能存在CPU执行指令)
  • n++,n–,++n,–n都不是原子性,要分解成三条指令,从内存读取变量到CPU,修改变量,写回内存
  • Object对象的new操作:Object obj = new Object();分解为三条指令:分配对象的内存,初始化对象,将对象赋值给变量
    b.可见性
new Thread(new Runnable(){
	@Override
	public void run(){
		for(int j = 0;j < COUNT;j++){
			SUM++;
		}
	}
}).start();
  • 从主内存中将SUM变量复制到线程的工作内存
  • 在工作内存中修改变量(+1操作)
  • 将SUM变量从线程的工作内存写回主内存
  • 在这里插入图片描述
    为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是可见性问题
    造成线程不安全,共享变量发生了修改的丢失
    c:重排序
    线程内代码是JVM,CPU都进行重排序,给我们的感觉就是线程内的代码是有序的,是因为重排序优化方案会保证线程内代码执行的依赖关系
  • 线程内看自己代码运行,都是有序的,但是看其他线程代码运行,都是无序的
  • 如果都是私有变量,最终结果是正确的,如果是共享变量,最终结果是错误的

3.结合JMM看待java进程运行,多线程不安全问题

java类名
类名的进程,启动—>执行java.exe进程

  • 初始化JVM参数
  • 创建JVM虚拟机
  • 启动后台线程
  • 启动java级别main线程(执行java main方法)

五、解决线程不安全的操作

1.synchronized关键字

线程安全用的,同步关键字
当线程释放锁时,JVM会把该线程对应的工作内存中的共享变量刷新到主内存中,当线程获取锁时,JVM会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量
作用:对一段代码加锁操作,让某段代码满足三个特性:原子性,可见性,有序性
原理:多个线程间同步互斥(一段代码在任意一个时间点,只有一个线程执行:加锁,释放锁)
加锁,释放锁:基于对象来进行加锁/释放锁,不是把代码锁了

1.1具体使用

a:静态方法上:对当前类对象进行加锁
b:实例方法上:对this对象加锁
c:代码块:Synchronized(对象){ 。。。}
注意事项:只有对同一个对象加锁,才会让线程产生同步互斥的作用

1.2.进入synchronized代码行,需要获取对象锁
  • 获取成功:往下执行代码
  • 获取失败:阻塞在synchronized代码行
    synchronized这行代码,加锁,}结束的代码行,释放锁
1.3.退出synchronized代码块,或synchronized方法
  • 退回对象锁
  • 通知JVM及系统,其他线程可以来竞争这把锁
1.4.注意事项:
  • synchronized用的锁是存在java对象头里的
  • synchronized同步块对同一条线程来说是可重入的,不会出现把自己锁死的问题
  • 同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入
  • 对一个共享变量的修改操作,大家都在同步修改,就要加锁了
  • synchronized(this),对当前对象加锁Rubbable加锁:线程不安全
  • synchronized(…class)对当前类对象加锁:线程安全
1.5.关注点:
  • 对哪个对象加锁—一个对象只有一把锁
  • 只有同一个对象,才会有同步互斥的作用(多线程线程安全的三大特性都能满足)
  • 对于synchronized内的代码来说,在同一个时间点,只有一个线程在运行(没有并发、并行)
  • 运行的线程越多,性能下降越快,归还对象锁的时候,线程不停的在被唤醒阻塞状态切换
  • 同步代码执行时间越短,性能下降也比较快
synchronized(safeThread.class){...}//对当前类对象加锁
等同于public static synchronized void increment(){...}
synchronized(this){...}//对this对象进行加锁
等同于public synchronized void increment2(){...}
1.6.执行过程图解

在这里插入图片描述
注意:唤醒是JVM和系统级别,程序还是阻塞的

1.7.多线程操作需要考虑的地方
  • 安全
  • 效率
    在保证安全的前提条件下,尽可能的提高效率:
    线程执行时间比较长,考虑多线程(线程的创建、销毁的时间消耗)
    如果不能保证安全,所有代码都没有意义—先安全,再效率
1.8.明确锁的是什么

锁的Synchronized对象

public class SynchronizedDemo{
	public synchronized void method(){
	}
	public static void main(String[] args){
		SynchronizedDemo demo = new SynchronizedDemo();
		demo.method();//进入方法会锁demo指向对象中的锁,出方法会释放demo指向的对象中的锁
	}
}
		

锁的SynchronizedDemo类的对象,线程间同步互斥

public class SynchronizedDemo{
	public synchronized static void method(){
	}
	public static void main(String[] args){
		nethod();//进入方法会锁SynchronizedDemo.class对象中的锁,出方法会释放SynchronizedDemo.class指向的对象中的锁
}

明确锁的对象

public class SynchronizedDemo{
	public void method(){
		//进入代码块会锁this指向对象中的锁,出代码块会释放this指向对象中的锁
		synchronized(this){
		}
	}
	public static void main(String[] args){
		SynchronizedDemo demo = new Synchronized();
		demo.methon();
	}
}

使用不同的对象加锁,没有同步互斥的作用

public static void increment(){
	synchronized(new synchronizedTest()){
		COUNT++;
	}
}

2.volatile关键字

(1)保证可见性
(2)禁止指令重排序,建立内存屏障
(3)不保证原子性

class ThreadDemo{
	private volatile int n;
}
2.1说明
  • 分解后的指令,有volatile修饰的变量,这行指令禁止重排序

  • volatile不能保证原子性,所以不能满足n++,n–操作的线程安全

  • volatile对变量进行赋值操作时,需要视常量(不能依赖变量)

2.2注意点
  • volatile保证可见性,保证有序性,不能保证原子性
  • volatile修饰的变量,进行赋值不能依赖变量(常量赋值可以保证线程安全)
2.3使用场景
  • 一般读写分离的操作,提高性能
    (1)写操作不依赖共享变量,赋值是一个常量
    (2)作用在读,写依赖其他手段(加锁)

六、多线程案例

1.单例模式

1.1饿汉模式

class Singleton {
	private static Singleton instance = new Singleton();
	private Singleton() {}
	public static Singleton getInstance() {
		return instance;
	}
}

1.2懒汉模式–单线程版

class Singleton {
	private static Singleton instance = null;
	private Singleton() {}
	public static Singleton getInstance() {
		if (instance == null) {
			instance = new Singleton();
		} 
		return instance;
	}
}

1.3 懒汉模式-多线程版-性能低
class Singleton {

private static Singleton instance = null;
private Singleton() {}
public synchronized static Singleton getInstance() {
	if (instance == null) {
		instance = new Singleton();
	} 
	return instance;
	}
} 

1.4 懒汉模式-多线程版-二次判断-性能高
满足:

  • 性能:初始化对象操作之后,其他线程进入第一行代码(并发并行)
  • 线程安全:同时初始化操作(多个线程同时进入第一行代码),使用加锁操作,再次校验:单例模式需要
class Singleton {
	private static volatile Singleton instance = null;
		private Singleton() {}
	public static Singleton getInstance() {
		if (instance == null) {
			synchronized (Singleton.class) {
				if (instance == null) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}

2.线程池

线程池最大的好处就是减少每次启动、销毁线程的损耗

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class ThreadPool {
	private static class Worker extends Thread {} 
	private BlockingQueue<Runnable> jobQueue;
	private int nCurrentThreads;
	private int nThreads;
	private Worker[] workers;
	public ThreadPool(int nThreads, int nCachedJobs) {
		this.jobQueue = new ArrayBlockingQueue<>(nCachedJobs);
		this.nCurrentThreads = 0;
		this.nThreads = nThreads;
		this.workers = new Worker[nThreads];
} 
	public void execute(Runnable command) throws InterruptedException {
		if (nCurrentThreads < nThreads) {
			Worker worker = new Worker();
			workers[nCurrentThreads++] = worker;
			worker.start();
		} else {
			jobQueue.put(command);
			}
		}
	}
}

七、总结

1.保证线程安全的思路

  1. 使用没有共享资源的模型
  2. 适用共享资源只读,不写的模型
  3. 不需要写共享资源的模型
  4. 使用不可变对象
  5. 直面线程安全(重点)
  6. 保证原子性
  7. 保证顺序性
  8. 保证可见性

2.对比线程和进程

2.1 线程的优点
  1. 创建一个新线程的代价要比创建一个新进程小得多
  2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  3. 线程占用的资源要比进程少很多
  4. 能充分利用多处理器的可并行数量
  5. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  7. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
2.2 进程与线程的区别
  1. 进程是系统进行资源分配和调度的一个独立单位,线程是程序执行的最小单位。
  2. 进程有自己的内存地址空间,线程只独享指令流执行的必要资源,如寄存器和栈。
  3. 由于同一进程的各线程间共享内存和文件资源,可以不通过内核进行直接通信。
  4. 线程的创建、切换及终止效率更高。

3.程序计数器

线程私有的,程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器

4.Java虚拟机栈

也是线程私有的,它的生命周期和线程相同,虚拟机栈描述的是Java方法执行 的线程内存模型,每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧,由于存储局部变量表,操作数栈,动态连接,方法和出口等信息,每一个方法被调用,直至执行完毕的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程

5.本地方法栈

线程私有的,与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务

6.java堆

线程共享的,所有的对象以及数组都是在堆上分配

7.方法区

线程共享的,与java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量(final修饰),静态变量(static修饰的),即时编译器编译后的代码缓存等数据

8.运行时常量池

线程共享的,运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中

9.直接内存

本地内存中的,直接内存并不是虚拟机运行时数据区的一部分,也不是<java虚拟机规范>中定义的内存区域,但是这部分内存也被频繁的使用,而且也可能导致OutOfMemoryError异常出现.
IO操作会将数据从本地内存中复制到java进程内存,效率比较差

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值