Java 多线程
一、定义
java程序本身就是多线程的。
二、相关概念
1、操作系统
内核实现其他资源的管理和调度。
2、进程
一个进程就是操作系统中运行的一个应用程序,每个进程都有进程ID标志唯一性
3、线程
线程是计算机进行调度的最小资源,线程运行一般也包含在进程中。
三、java中的线程
在java中Thread就表示线程
注意:
1. 启动线程不能直接调用run方法,而是调用start启动。
2. 虽然在Thread中提供了stop终止线程,但是可能会出现不可控问题,不推荐使用;
应该通过程序逻辑控制线程的终止条件。
3. 如果线程运行过程中出现异常也会导致线程异常终止
1、java中创建线程的方法
1)通过一个类继承Thread类。调用创建对象的start方法启动线程。
案例代码:
public class ThreadDemo01 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
//此处不应使用run方法运行线程,应该使用start启动线程
myThread.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
//当调用start方法时,启动线程运行的逻辑代码
System.out.println(1);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(2);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(3);
}
}
注意:这种创建线程的方法不推荐使用,因为java支持的是单继承,这里会导致
类的扩展性降低。
2)使用类实现接口Runnable,并实现run方法。将类创建的对象传递给Thread构造。
案例:
public class ThreadDemo02 {
public static void main(String[] args) {
MyThead1 myThead1 = new MyThead1();
Thread thread = new Thread(myThead1);
thread.start();
}
}
class MyThead1 implements Runnable{
@Override
public void run() {
System.out.println(1);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(2);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(3);
}
}
3)创建一个类实现JUC(Java并发工具包)下的Callable<V>接口,泛型V表示
线程运行结束后返回的结果类型。
案例代码:
public class ThreadDemo04 {
public static void main(String[] args) throws Exception {
int question = 50;
AStudent aStudent = new AStudent(question);
FutureTask<String> stringFutureTask = new FutureTask<>(aStudent);
Thread thread = new Thread(stringFutureTask);
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//通过FutureTask获取执行的结果
String result = stringFutureTask.get();
System.out.println(result);
}
}
class AStudent implements Callable<String> {
private int question;
public AStudent(int question){
this.question = question;
}
@Override
public String call() throws Exception {
while (this.question>0){
System.out.println("A同学当前回答的问题是:"+this.question--);
}
return "A同学回答完毕!";
}
}
2、线程类Thread相关方法
构造方法:
Thread()
Thread(Runnable target)
常用方法:
Thread.currentThread()//获取当前线程
String getName() //获取线程的名字
void setName(String name) //设置线程的名称
Long getId() //获取线程ID
final void setDaemon(boolean on) //是否设置为守护线程
Thread.activeCount() //当前活动的线程数
Thread.sleep(tm) //设置睡眠时间
3、Object类中关于线程状态相关方法
final native void notify() //通知
final native void notifyAll(); //通知所有
final native void wait(long timeout) //等待,timeout表示等待超时时间
final void wait() //等待
final void stop() //终止线程(不推荐使用)
四、线程运行状态
1、创建 NEW
实例化线程对象时(初始)
2、就绪、运行 RUNNING
就绪: 实现化线程对象并调用了start方法(等待分配cpu运行资源)
运行:线程分配到了运行资源,只有就绪状态的线程才可以进入运行状态
3、线程终止 TERMINATION
stop(),线程正常执行结束,运行时出现异常导致线程终止。
4、阻塞 BLOCKING
当线程遇到synchronized并且没有抢占到锁资源时
5、限时等待 WAITTING TIMEOUT
线程调用了类似于wait(timeout),join()线程加入时会进入限时等待。当等待超时后线程
会重新进入到就绪状态。
6、等待 WAITTING
调用了wait()进入等待,在等待期间会释放锁资源并且不抢占cpu资源。只有被其他线程
通知notify()时才会被唤醒。
wait()线程等待,sleep()睡眠:
被设置为wait()的线程,不会争抢CPU资源,而且还会释放锁
sleep()睡眠,不会争抢CPU,而且不会释放锁
五、多线程并发安全问题
1、定义
如果多个线程在对同一个资源进行修改操作时,就可能会产生并发安全问题。
2、如何处理并发安全问题
1-使用同步代码块
synchronized(对象-用来承载锁的对象){
//将需要同步的代码包裹起来
}
注意:
用作承载锁的对象必须是所有的线程都能看到的同一个对象
同步代码块一次只有一个线程在其中执行,会降低效率,所以在设
计同步代码块的时候,应该在能包裹住会造成线程并发安全问题代码
的前提下尽量少的包裹其他代码,减少在同步中执行的时间,可以整体的提升效率
同步方法:
方法中所有代码都需要上锁,可以将当前方法设置为同步方法;在方法前加上synchronized。
如果是同步的非静态方法,锁对象选择的就是this。
同步的静态方法,锁对象是当前类的class字节码。
六、死锁
锁之间互相等待无法进行下去的状态就称为产生了死锁。
如何解决死锁?
* 其中锁占用一方强制退出
* 等待超时机制
案例:打印机和扫描仪相互抢占导致死锁
public class Thread08 {
public static void main(String[] args) {
Thread p1 = new Thread(new Person1());
Thread p2 = new Thread(new Person2());
p1.start();
p2.start();
}
}
//打印机
class Printers {
}
//扫描仪
class Scanners {
}
class Person1 implements Runnable{
public void run() {
synchronized (Printers.class) {
System.out.println("Person1 正在使用打印机...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (Scanners.class) {
System.out.println("Person1 正在使用扫描仪...");
}
}
}
}
class Person2 implements Runnable{
public void run() {
synchronized (Scanners.class) {
System.out.println("Person2 正在使用扫描仪...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0; i<10; i++) {
if(i==6) {
throw new RuntimeException("no");
}
}
synchronized (Printers.class) {
System.out.println("Person2 正在使用打印机...");
}
}
}
}
七、关于线程的其他细节
1、守护线程
线程可以分为前台线程和后台线程。
如果一个线程被设置为守护线程后,当前台没有线程时,哪怕没有执行完也会自动退出。
设置守护线程的方式: 在运行线程前调用setDaemon方法
方法:
void setDaemon(boolean on) //默认为false,true表示设置为守护(后台)线程
注意:设置守护线程的方法,必须要在调用start方法之前执行。
案例代码:
public class ThreadDemo07 {
public static void main(String[] args) {
Person person = new Person();
Thread thread = new Thread(person);
thread.setDaemon(true);
thread.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"线程已经执行结束");
}
}
class Person implements Runnable{
@Override
public void run() {
for (int i=1; i<=10; i++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前执行的编号为:"+i);
}
}
}
2、线程加入
当正在执行的线程执行到了join(),就会将执行权让给调用此方法的线程,自己转入冻结状态,
直到调用此方法的线程结束,当前线程才会从冻结状态醒来,接着执行。
方法:
final void join()
案例代码:炒菜
public class ThreadDemo08 {
public static void main(String[] args) throws Exception {
System.out.println("准备炒菜~~");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("菜没了~~");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Buy buy = new Buy();
Thread thread = new Thread(buy);
thread.start();
thread.join();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("继续炒菜~");
}
}
class Buy implements Runnable{
@Override
public void run() {
System.out.println("准备出门~~~");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("买菜~~~");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("回家~~~");
}
}
3、线程退让:
让当前线程暂停执行,可以执行其他线程
Thread.yield();
4、线程执行的优先级
修改线程的优先级 1 - 10,越大优先级越高,但是这仅仅是理论上的优先级,真正执行时,
谁执行谁不执行,仍然是概率的,只是概率高低有所不同。通常 1、5、10表现的效果比较明显。
线程默认优先级都是5。
方法:
void setPriority(int newPriority) //优先级:1~10
final int getPriority() //获取优先级
扩展:java的内存模型-》多线程(并发编程)
八、volatile关键字
1、作用
关键字volatile的主要作用是使变量在多个线程间可见。
2、特点
保证可见性:
也就是说某个线程修改了共享变量,线程在修改变量时不会把值缓存在寄存器或者其他地方,而
是会把值刷新回主内存。当其它线程读取该共享变量时,会从主内存重新获取变量最新值,而不是
从当前线程的工作内存中获取。
保证有序性(禁止指令重排序):
所谓指令重排序:为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。
3、volatile与synchronized的区别
-关键字volatile是线程同步的轻量级实现,所以volatile的性能略胜于synchronized,
并且volatile只能修饰变量,而synchronized可以修饰方法、代码块等;
-多线程访问volatile不会发生阻塞,而synchronized会出现阻塞;
-volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,
也可以间接保证可见性,因为它会将私有内存和共有内存中的数据做同步;
-关键字volatile解决的是变量在多个线程之间的可见性;而synchronized关键字解决的
是多个线程之间访问资源的同步性。
九、ThreadPoolExecutor
1、构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
参数介绍:
int corePoolSize:核心线程大小,线程池最小的线程数量。
int maximumPoolSize:最大线程大小,线程池的最大的线程数量
long keepAliveTime:超过corePoolSize的线程在不活动的状态下存活的最大时间。
TimeUnit unit:keepAliveTime的时间单位。
BlockingQueue<Runnable> workQueue:任务队列。
ThreadFactory threadFactory:线程池工厂,用来创建线程。
RejectedExecutionHandler handler:拒绝策略。
2、执行流程
(1)当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。
(2)当线程池超过corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行。
(3)当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务。
(4)当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理。
(5)当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,释放空闲线程。
(6)当设置allowCoreThreadTimeOut(true)时,该参数默认false,线程池中corePoolSize线程空闲时间达到keepAliveTime也将被关闭。
3、拒绝策略
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常,这是默认的一种方式。
ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务。
4、线程池设置
**cpu密集型**
cpu密集的意思是该任务需要大量的运算,而没有阻塞,cpu一直全速运行。
cpu密集型任务配置尽可能少的线程数量:以保证每个cpu高效的运行一个线程。
通常设置方式:
与cpu核心数相同的线程数或者加1
**io密集型**
io密集型,即该任务需要大量的io,即大量的阻塞。
在单线程上运行io密集型的任务会导致浪费大量的cpu运算能力浪费在等待。所以在io密集型任务中使用多线程可以大大的加速程序运行,这种加速主要就是利用了被浪费掉的阻塞时间。
通常设置方式:
cpu核数 * 2个线程的线程池。
**获取核心数**
Runtime.getRuntime().availableProcessors();
5、使用Executors创建线程池:
1. Executors.newCachedThreadPool
是一个根据需要创建新线程的线程池,当一个任务提交时,corePoolSize为0不创建核心线程,SynchronousQueue是一个不存储元素的队列,可以理解为队里永远是满的,因此最终会创建非核心线程来执行任务。
2. Executors.newSingleThreadExecutor
单线程线程池,只有一个核心线程,用唯一的一个共用线程执行任务,保证所有任务按指定顺序执行
3. Executors.newFixedThreadPool
定长线程池,核心线程数和最大线程数由用户传入,可以设置线程的最大并发数,超出在队列等待
4. Executors.newScheduledThreadPool
定长线程池,核心线程数由用户传入,支持定时和周期任务执行
总结:
FixedThreadPool和SingleThreadExecutor 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积
大量的请求,从而引起OOM异常
CachedThreadPool 和newScheduledThreadPool允许创建的线程数为Integer.MAX_VALUE,可能会创
建大量的线程,从而引起OOM异常