Java - 多线程与锁
进程-线程
进程,Process,处于运行中的程序,系统进行资源分配和调度的独立单位,拥有独立的内存空间(堆)。
- 动态性:生命周期和状态;
- 独立性:独立实体;
- 并发性:Concurrency,抢占式多任务操作策略;
注:并发性是同一时刻只有一条指令执行,但是CPU是在多进程间快速切换;并行性,parallel,是同一时刻有多条指令在多个处理器上同时执行。
线程,Thread,(轻量级进程,LightWeight Process),线程是进程的执行单元,在进程(程序)中是独立的、并发的执行流。线程是抢占式执行的,一个线程可以创建和撤销另一个线程。
线程是进程的组成部分,线程的调度和管理由进程负责。一个线程必须有一个父进程,一个进程可以拥有多个线程,多个线程共享进程的全部(系统)资源、进程代码段等,但是线程有独立的堆栈、寄存器集合、局部变量、程序计数器等,不同线程之间相互独立。线程在进程(程序)中的地位,等同于进程在操作系统中的地位。
操作系统可以执行多个任务(进程),进程可以并发处理多个任务(线程)。
多线程,隔离程度小、并发性能好。
- 进程创建代价大;
- 线程共享进程内存空间;
参见:线程总结
创建线程
继承java.lang.Thread类,重写run()方法
public class Thread implements Runnable { private Runnable target; public Thread() {} public synchronized void start() {} @Override public void run() {} private void exit() {} public final native boolean isAlive(); }
特点
- 简单易实现,利用this直接获取当前线程;
- 线程类已经继承了Thread类,不能再继承其他类;
- 线程类的多个线程无法共享线程类的实例变量;
步骤
- 定义Thread类的派生类,重写Thread类的run()方法;
- 创建Thread派生类的实例(线程对象);
- 调用线程对象的start()方法,启动线程;
public class MyThread extends Thread { public void run(){ // 线程执行体 System.out.println( getName() + ":" + getId() ); } public static void main(String[] args){ System.out.println(Thread.currentThread().getName()); MyThread myThread = new MyThread(); myThread.start(); } }
实现java.lang.Runnable接口,实现run()方法
@FunctionalInterface public interface Runnable { public abstract void run(); }
特点
- 实现较复杂,必须利用Thread.currentThread()方法访问当前线程;
- 线程类只是实现了Runnable/Callable接口,还可以继承其他类,避免Java单继承的限制;
- 线程类的多个线程可以共享同一个target对象(共享线程类[实际上应该是线程的target类]的实例变量);
- 代码可以被多个线程共享,代码和数据独立,适合多个相同线程访问同一资源;
- Runnable接口是函数式接口,可以利用Lambda表达式创建Runnable对象;
步骤
- 定义Runnable接口的实现类,实现Runnable接口的run()方法;
- 创建Runnable接口实现类的实例,并以此实例作为Thread的target来创建Thread对象;
- 调用线程对象的start()方法,启动线程;
public class MyRunnable implements Runnable { public void run(){ // 线程执行体 System.out.println(Thread.currentThread().getName()); } public static void main(String[] args){ System.out.println(Thread.currentThread().getName()); MyRunnable myRunnable = new MyRunnable(); Thread myThread = new Thread(myRunnable, "sqh-Runnable"); myThread.start(); } }
实现java.lang.Callable接口,结合Future和线程池
特点
- 同Runnable;
- Callable接口是函数式接口,可以利用Lambda表达式创建Callable对象;
步骤
- 定义Callable接口的实现类,实现Callable接口的call()方法;
- 创建Callable接口实现类的实例,并用FutureTask类封装Callable对象;
- 利用FutureTask对象作为Thread对象的target创建线程Thread对象,并调用线程对象的start()方法,启动线程;
- 通过FutureTask对象的get()方法获取call()方法的返回值;
其中,call()方法可以有返回值,可以声明抛出异常。
public class MyCallable implements Callable<String> { public String call() throws Exception { // 线程执行体 return Thread.currentThread().getName(); } public static void main(String[] args){ System.out.println(Thread.currentThread().getName()); MyCallable myCallable = new MyCallable(); FutureTask<String> ft = new FutureTask<String>(myCallable); Thread myThread = new Thread(ft, "sqh-Callable").start(); try{ System.out.println(ft.get()); } catch(Exception e){ e.printStackTrace(); } } }
线程状态
- 新生状态(New)
- 就绪状态(Runnable)
- 运行状态(Running)
- 阻塞状态(Blocked)
- 死亡状态(Dead)
锁
每个 Java 对象都有一个(内置的)锁对象,而且只有一把钥匙。创建锁对象:
- this 关键字
- 类名.class
- 对象.getClass()
注意以下几个小知识点
- 线程进入 Sleep() 睡眠状态时,线程仍然持有锁、不会释放
- 同步方法会影响性能(死锁),优先考虑同步代码块
死锁
当多个线程完成某个功能需要同时获取多个共享资源的时候可能会导致死锁
两个任务以相反的顺序申请两个锁:线程T1获得锁L1,线程T2获得锁L2,然后T1申请获得锁L2,同时T2申请获得锁L1。
避免死锁
线程通信
等待唤醒机制:Java 通过 Object 类的 wait,notify,notifyAll 方法实现线程间通信
- wait:告诉当前线程放弃执行权,并放弃锁、进入阻塞状态,直到其他线程获得执行权,并持有相同的锁、调用notify为止
- notify:唤醒持有同一个锁的调用了 wait 的第一个线程,该线程进入可运行状态、等待获取执行权
- notifyAll:唤醒持有同一个锁的调用了 wait 的所有的线程
其中,操作线程的等待和唤醒必须是针对同一个锁对象(锁可以是任一对象,Object 类)。
注意,sleep() 和 wait() 方法的区别
- sleep():释放资源,不释放锁,Thread的方法
- wait(): 释放资源,释放锁,Object的方法
线程间通信,其实就是多个线程在操作同一个资源,但操作动作不同。
关于线程的 join() 方法
join 可以用来临时加入线程执行:当A线程执行到了B线程join方法时,A就会等待,直到B线程都执行完、A才会执行。