java多线程+锁
一、概念
- 程序:program,是为完成特定的任务或需求,而用某种语言编写的一组指令的集合。指一段静态代码,静态对象。
- 进程:process,是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程,有它的产生、存在、消亡等。进程作为资源分配的单位
- 线程:Thread,进程可进一步细化为线程,是一个程序内部的一条执行路径。
- 多线程:一个进程同一时间并行执行多个线程。
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc)。
- 一个进程中的线程共享相同的内存单元/内存地址空间->它们从同一堆中分配对象,可以访问相同的变量和对象,这就使得线程间通信更简单、高效。
- 多个线程操作共享的系统资源可能带来安全的隐患。
1.单核CPU与多核CPU
- 单核CPU:一种假的多线程。因为在一个时间单元内内,也只能执行一个线程的任务。(比如:收费站有多车道,但只有一个收费员,只有收费才能通过,否则就被挂起等待),因为CPU时间单元特别短,所以感觉不出来。
- 多核CPU:现在大多是多核(服务器),才能更好的发挥多线程的效率。
- 一个java程序,至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。如果发生异常会影响主线程。
2.并行与并发
- 并行:多个CPU同时执行多个任务。(多个人同时做不同事情)
- 并发:一个CPU(采用时间片)同时执行多个任务。(秒杀、多个人做同一件事)
二、优点
背景:以单核CPU为例,使用单个线程先后完成多个任务比用多线程用时更短,为何仍需多线程?
优点:
- 1.提高应用程序的响应,对图形界面有意义,增强用户体验
- 2.提高计算机系统CPU利用率
- 3.改善程序结构,将长的分为多个独立运行,便于理解修改
三、使用场景
需求:
- 程序需要执行两个或多个任务
- 程序需执行等待的操作,如:用户输入、文件读写、网络操作、搜索等
- 需要一些后台运行程序时
四、线程的创建和使用
java语言允许程序运行多个线程,通过java.lang.Thread类体现。
Thread类特性:
- 每个线程都是通过Thread对象的run()方法完成操作,run()方法体称为:线程体。
- 通过该Thread对象的start()方法来启动这个线程,而非直接调用run方法。
1.Thread类
java.lang.Thread
构造器:
- Thread():创建新的对象
- Thread(String threadName):创建线程并指定线程实例名
- Thread(Runnable target):指定线程的目标对象,它实现了runnable接口的run方法
- Thread(Runnable target,String name):创建新的Thread对象
2.Thread类的方法
Thread类有关方法
- void start():启动线程,并执行对象的run()方法
- run():线程在被调度时执行的操作
- String getName():返回线程的名称
- void setName():设置线程的名称
- static Thread currentThread():返回当前线程。在Thread子类就时this,通常用于主线程和Runnable实现类
- static void yield() :线程让步
》暂停当前正在执行的线程,把机会让给优先级相同或者更高的线程
》若队列中没有同优先级的线程,忽略此方法
- join():当某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到join()方法加入的join线程执行完为止
》低优先级的线程也可以获得执行
- static void sleep(long millis):(指定时间:毫秒)
》令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队
》抛出InterruptedException异常
- stop():强制线程生命期结束
- boolean isAlive():返回boolean,判断线程是否还活着
3.创建方式
方式一:继承Thread类
- 1.子类继承Thread类
- 2.子类重写thread类的run方法
- 3.创建子类对象。即创建线程对象
- 4.调用start方法,启动线程,执行run方法
注意
- 手动调用run,只是普通方法
- run方法由JVM调用,什么时候调用,执行过程的控制都有系统的CPU的调度决定
- 启动多线程,必须用start
- 一个线程对象只能调用一次start,如果重复调用,抛出
lllegalThreadStateException异常
方式二:实现Runnable接口
- 1.定义子类,实现Runnable接口
- 2.子类重写Runnable接口的run方法
- 3.通过Thread类含参构造器创建线程对象
- 4.将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
- 5.调用Thread类的start方法,启动线程,执行Runnable接口的run方法
两种方式的区别
继承方式与实现方式的区别
区别
- 继承Thread:线程代码存放在Thread类的run方法中
- 实现Runnable:线程代码存放在接口的子类的run方法。
实现方式的好处
- 避免了单继承的局限
- 多个线程可以共享一个接口实现类的对象,非常适合多个相同的线程来处理同一份资源。
4.线程的调度
两种调度方式:
-
分时调度模型:所有线程轮流使用CPU使用,平均分配每个线程占用Cpu的时间片
-
抢占式调度模型:优先让优先级高的线程使用CPU,如果优先级相同,随机分配,优先级高的获取CPU时间片相对多
java 的调度方法
- 同优先级线程组成先进先出队列(先到先服务),使用时间片策略。
- 对高优先级,使用优先调度的抢占式策略。
5.线程优先级
线程的优先等级
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRIORITY:5
涉及方法
方法 | 说明 |
---|---|
final int getPriority | 返回此线程的优先级 |
final void setPriority(int newPriority) | 更改此线程的优先级,默认5,1~10 |
static void sleep(long millis) | 暂停 |
join | 等待死亡 |
setDaemon | 守护线程,全为守护线程JVM退出 |
说明
- 线程创建时继承父线程的优先级
- 低优先级只是获得调度的概率低,并不绝对在高优先级之后执行
五、线程分类
java中线程分为两类:
守护线程+用户线程
说明
- 几乎相同,唯一区别:判断JVM何时离开
- 守护线程是用来服务用户线程的,通过在start方法前调用thread.setDaemon(true)可以把一个用户线程变成守护线程。
- java垃圾回收就是典型的守护线程
- 若JVM都是守护线程,当前JVM将退出
- 形象理解:兔死狗烹,鸟尽弓藏
六、线程的声明周期
JDK中用Thread。state类定义了线程的集中状态
- 新建:当Thread以及子类被创建对象,新生线程对象处于新建状态。
- 就绪:新建线程被start后,将进入线程队列等待CPU时间片,具备运行条件,未分配到CPU资源
- 运行:就绪线程获得CPU资源时,便进入运行状态,run方法定义了线程的操作功能
- 阻塞:某种情况,被人挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
- 死亡:线程完成了全部工作,或被提前强制中止或异常结束。
七、线程状态转换图
八、线程的同步
1.同步
java对于多线程安全问题提供专业解决方式:同步机制
同步锁机制
在《Thinking in java》中,是这么说的:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。防止这种冲突做法:当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。
注意
- 必须确保使用同一资源的多个线程共用一把锁,非常重要。否则无法保证共享资源的安全
- 一个线程类中所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)
同步的范围
1.明确哪些是多线程执行的代码
2.多个线程是否有共享的数据
3.明确多个线程运行代码中是否有多条语句操作共享数据
所有操作共享数据的这些语句都要放在同步范围中
释放锁的操作
1.当前线程的同步方法、同步代码块执行结束
2.线程在执行中遇到break、return代码
3.出现未处理的Error、Exception,导致异常结束
4.执行了当前线程对象的wait方法,当前线程暂停,释放锁
不会释放锁的操作
1.线程执行同步代码块或同步方法时,调用Thread.sleep()\Thread.yield()方法暂停当前线程执行
2.线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)
应尽量避免使用suspend和resume来控制线程
2.synchronized
synchronized的锁是什么?
- 任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)
- 同步方法的锁:静态方法(类名.class),非静态方法(this)
- 同步代码块:自己指定,很多时候是this或类名.class
使用方法:
同步代码块:
synchronized (对象){
//需要同步的代码;
}
同步方法:放在方法声明
public synchronized void show(){
…
}
3. 懒汉式
public class Singleton {
//私有化构造器
private Singleton(){
}
//提供一个当前类的实例
//此实例必须静态化
private static Singleton single;
//提供公共静态方法,返回当前类对象
public static Singleton getInstance(){
if (single==null){
synchronized (Singleton.class){
if(single==null){
single=new Singleton();
}
}
}
return single;
}
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1==instance2);
}
}
4.死锁
- 不同线程分别占用对方需要同步的资源,不放弃,都在等待对方让步,形成线程死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
解决方法
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
5.Lock锁
Lock锁
- JDK5.0开始,加入通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
- ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义。比较常用ReentrantLock,可以显示加锁,释放锁
public class ReentrantTest {
private final ReentrantLock lock = new ReentrantLock();
public void show(){
//加锁
lock.lock();
try{
//代码
}finally {
//释放锁
lock.unlock();
}
}
}
6.synchronized与Lock锁比较
- Lock是显示锁(手动开启和释放锁),synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块和方法锁
- 使用Lock锁,JVM将花费较少时间来调度来调度线程,性能更好。并具有良好扩展性(提供更多的子类)
优先使用顺序
Lock->同步代码块(已经进入方法体,分配了相应资源)->同步方法(在方法体外)
7.线程的通信
wait()、notify()、notifyAll()
- wait() :令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程等待其他线程调用notify和notifyAll方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行
- notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待。
- noyifyAll():唤醒正在排队等候资源的所有线程结束等待
这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则报java.lang.lllegalMonitorstateException异常
- 因为这三个方法必须有锁对象调用,而任意对象都可作为synchronized的同步锁,因此这三个方法只能声明在Object类中
九、JDK5.0新增两种线程
1.Callable
与Runnable相比
- 相比run方法,有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask类,比如获取返回结果
Future接口
- 可以对具体的Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等
- FutureTask是Future接口唯一的实现类
- FutureTask同时实现了Runnable,Future接口,它既可作为Runnable被线程执行,又可以作为Future得到Callable的返回值
public class CallabaleTest implements Callable{
@Override
public String call() throws Exception {
return "我是"+Thread.currentThread().getName();
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask = new FutureTask<>(new CallabaleTest());
Thread thread = new Thread(futureTask);
thread.setName("callable");
thread.start();
String s = futureTask.get();
System.out.println(s);
}
}
2.线程池
- 背景:经常创建、销毁、使用量特别大的资源
- 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回。可以避免频繁创建、销毁,实现重复利用。
- 好处:提高响应速度(减少创建时间)、降低资源消耗、便于线程管理:
corePoolSize:核心池的大小 maximumPoolSize:最大线程数 keepAliveTime:无任务时线程最多> 保持时间会终止
- 线程池相关API
ExecutorService和Excutors ExecutorService:真正线程池接口。常见子类:ThreadPoolEcecutor Excutors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
知识图谱
在线查看:java多线程
欢迎一键三连,私信我领取知识图谱!