多线程
基本概念
进程与线程
#### 基本定义
进程是指一个拥有独立内存空间的应用程序,是资源分配的基本单位。在程序运行时,系统会创建一个进程并为它分配资源,再把它放在进程就绪队列,然后等待进程调度器调度,最后真正运行程序。
线程是进程中的一个执行路径,一个进程可以有多个线程组成,线程间共享进程所有的内存资源,每个线程有自己的堆栈(运行时段)和局部变量。线程之间由CPU调度切换,从而可实现并发执行。多核CPU支持多个线程同时执行,所以多线程就是有多条执行路径同时执行。
二者之间的关系
线程实际是在进程的基础上进一步划分的,一个程序就是一个进程,程序中的多个任务可以被称为线程,进程是资源分配的基本单位,线程是操作系统调度执行的最小单位。处理机上真正运行的是线程,不同进程的线程要通过消息通信实现同步。
注意
多线程并不能提高程序的运行速度,但能提高程序的运行效率,让CPU的使用率更高。
线程分时调度
-
分时调度
所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。
-
抢占式调度
优先让优先级高的线程使用CPU,Java使用抢占式调度。
同步与异步
同步:排队执行,效率低但是安全
异步: 同时执行,效率高但是数据不安全
并发与并行
并发: 是指两个或多个事件在同一时间段内发生。
并行: 指两个或多个事件在同一时刻发生(同时发生)
守护线程与用户线程
Java程序默认启动的是用户线程,用户线程是独立存在的,不会因为其他用户线程的结束而结束。
守护线程是守护用户线程的,当所有的用户线程结束后,守护线程就会结束。在线程启动之前,可以调用setDemon(true)
将这个线程设置为守护线程。
线程
线程创建
继承Thread类
//1.创建线程继承Thread类,重写run方法
//2.创建线程对象
//3.调用start()方法,执行线程
public class Demo4 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
class MyThread extends Thread{
@Override
public void run(){
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
}
}
//Thread类
public class Thread implements Runnable {
// 构造方法
public Thread(Runnable target);
public Thread(Runnable target, String name);
public synchronized void start();
}
实现Runnable接口
//1.构造任务类,实现Runnable接口,重写run方法
//2.创建任务对象
//3.传递任务对象参数,创建一个线程,为其分配任务
//4.调用start()方法执行这个线程
public class Demo5 {
public static void main(String[] args) {
MyRunnable r = new MyRunnable();
Thread thread = new Thread(r,"线程1");
thread.start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
}
}
继承Thread类和实现Runnable接口创建线程都要重写run方法和调用start()方法来启动线程,所以二者方法本质上没有什么区别。但方式一,继承了Thread类就不能再继承其他类了,受到了单继承的局限性。
而方式二有以下优势
- 通过创建任务,然后给线程分配的方式来实现多线程,更适合多个线程同时执行多个相同任务的情况。
- 可以避免单继承带来的局限性,更加灵活。
- 任务与线程分离,提高了程序的健壮性
- 线程池技术,接受Runnable类型的任务,不接受Thread类型线程。
实现Callable
Callable:有返回值的线程,能取消线程,可以判断线程是否执行完毕。
//1.编写类实现Callable接口,实现call方法
//2.创建FutureTask对象,并传递第一步编写的Callable类对象
//3.通过Thread类,启动线程
public class Demo6 {
public static void main(String[] args) {
MyCallable c = new MyCallable();
FutureTask task = new FutureTask(c);
Thread thread = new Thread(task, "线程3");
thread.start();
}
}
class MyCallable implements Callable {
@Override
public Integer call() throws Exception {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
return 100;
}
}
//方式三创建的线程可以借助FutureTask类下的常用方法
// 取消线程
public boolean cancel(boolean mayInterruptIfRunning);
// 判断线程
public boolean isDone();
// 获取线程执行结果
public V get() throws InterruptedException, ExecutionException;
总结
/**
*三种方式在本质上都是实现了Runnable接口,方式一建议少使用,因为不够灵活;如果线程不需要返回值就使用Runnable,线程需要返回值就*使用Callable.
*/
//Thread类常用方法
static native Thread currentThread(); //返回当前正在执行线程对象的引用
synchronized void start(); //启动线程
static void sleep(); //让线程睡眠一会儿
void interrupt();// 打断线程 用于停止线程
final void setPriority(int newPriority);//设置线程优先级,有三个档次MAX_PRIORITY,MIN_PRIORITY,NORM_PRIORITY
final void setDaemon(boolean on);//设置是否为守护线程
long getId();//设置线程Id
String getName();//得到线程名称
void setName();//设置线程名称
线程状态
NEW(初始)
创建了新的线程对象,但还没有调用start()方法。
RUNNABLE(运行)
ready
ready指的是线程有运行的资格,但调度程序还没有挑选到它,有以下几种情况
- 调用线程的start()方法,进入就绪状态。
- 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
- 线程调用yield()方法,重新进入就绪状态。
running
线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。
BLOCKED(阻塞)
线程阻塞在进入synchronized修饰的方法或代码块时的状态。
WAITING(等待)
通常由wait()和join()造成,需要notify()或 notifyAll()唤醒。
线程进入等待状态,它们要等待被显式地唤醒,否则会处于无限期等待的状态。
TIMED_WAITING(限时等待)
常见的由sleep(long)、wait()和join()造成。
一个正在限时等待另一个线程执行一个动作的线程处于这一状态。它可以被唤醒,也可以等时间结束时,自动启动。
TERMINATED(终止)
线程销毁(正常执行完毕、发生异常或者被打断interrupt()都会导致线程终止)
线程安全
synchronized(隐式锁)
-
用synchronized修饰执行代码块来实现线程安全
public class Demo5 { public static void main(String[] args) { MyRunnable r = new MyRunnable(); Thread thread1 = new Thread(r,"线程1"); Thread thread2 = new Thread(r,"线程2"); thread2.start(); thread1.start(); } } class MyRunnable implements Runnable { private int count = 10; Object o = new Object(); @Override public void run() { while (true){ synchronized(o) { if (count > 0) { System.out.println(Thread.currentThread().getName() + "准备买票"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } count--; System.out.println(Thread.currentThread().getName() + "售票成功,余票为: " + count); } } } } }
- 用synchronized修饰同步方法来实现线程安全
public class Demo5 { public static void main(String[] args) { MyRunnable r = new MyRunnable(); Thread thread1 = new Thread(r,"线程1"); Thread thread2 = new Thread(r,"线程2"); thread2.start(); thread1.start(); } } class MyRunnable implements Runnable { private int count = 10; @Override public void run() { while (true) { boolean flag = ticket(); if(!flag){ break; } } } public synchronized boolean ticket(){ if (count > 0){ System.out.println(Thread.currentThread().getName() + "准备卖票"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } count--; System.out.println(Thread.currentThread().getName() + "售票成功,余票为:"+ count); return true; } return false; } }
使用synchronized关键字可以锁住代码块和方法,是隐式锁,不可中断,会自动释放(是因为JVM层面啥都帮我们做了)。
Lock(显式锁)
Lock 子类 ReentrantLock
public class Demo5 {
public static void main(String[] args) {
MyRunnable r = new MyRunnable();
new Thread(r,"线程1").start();
new Thread(r,"线程2").start();
new Thread(r,"线程3").start();
}
}
class MyRunnable implements Runnable {
private int count = 10;
Lock l = new ReentrantLock(true);//创建锁对象并设置为公平锁
@Override
public void run() {
while (true) {
l.lock();//锁住
try{
if (count > 0){
System.out.println(Thread.currentThread().getName() + "准备卖票");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName() + "售票成功,余票为:"+ count);
}else {
break;
}
}finally{
l.unlock();//解锁
}
}
}
}
Lock是一个接口,是JDK层面的有丰富的API。Lock只能锁住代码块,必须手段释放锁,可以中断也可以不中断,可以使用读锁提高多线程读效率。
公平锁和非公平锁
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
- 优点:所有的线程都能得到资源,不会饿死在队列中。
- 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,CPU唤醒阻塞线程的开销会很大。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到锁的线程进入等待队列,如果能获取到,就占得锁。
- 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,从而减少唤起线程的数量。
- 缺点:可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
注: synchronized是非公平锁,而ReentrantLock可以控制是否是公平锁。
线程死锁
指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。因此在任何可能有锁产生的方法里,不要调用另外方法让另外一个锁产生。
线程池Executors
线程池是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量资源和时间。
优势
- 降低资源消耗
- 提高线程执行的响应速度
- 提高线程的可管理性
缓存线程池
//(长度无限制)
//1.判断线程池是否存在空闲线程
//2.存在则使用
//3.不存在,创建线程并放入线程池中,然后再使用
//创建线程池
ExecutorService service = Executors.newCachedThreadPool();
//加入新任务
service.execute();
定长线程池
//(长度在创建时设置)
//1.判断线程池是否存在空闲线程
//2.存在则使用
//3.不存在空闲线程,则在线程池未满的情况下,创建线程并放入线程池中,然后使用。
//4.否则等待线程存在空闲线程
//创建线程池
ExecutorService service = Executors.newFixedThreadPool(5);//5为线程池的长度
//加入新任务
service.execute();
单线程线程池
//(创建时传入数值1)
//1.判断线程池是否存在空闲线程
//2.存在则使用
//3.不存在,则等待池中的单个线程空闲后使用
//创建线程池
ExecutorService service = Executors.newSingleThreadExecutor();
//加入新任务
service.execute();
周期定长线程池
//(有周期任务,其他与定长线程池一样)
//1.判断线程池是否存在空闲线程
//2.存在则使用
//3.不存在空闲线程,则在线程池未满的情况下,创建线程并放入线程池中,然后使用。
//4.否则等待线程存在空闲线程
//创建线程池
ScheduledExecutorService service = Executors.newScheduledThreadPool(5);
//1.可以定时执行一次
schedule(Runnable command, long delay, TimeUnit unit);
//2提交定期操作
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)