1、什么是线程?进程?
- 一个进程代表着一个整体应用程序,一般由操作系统来负责管理;线程是一个进程的一个执行单元或执行场景;
- 比如我们写一个Java的hello world程序运行,这时候就启动了一个JVM进程,而我们的main方法就是这个进程中的一个线程(这是主线程),它负责输出hello world,而同时JVM进程还有其他线程在运行(垃圾回收GC进程),没有其它那些线程运行我们的main方法线程也不能跑起来。
2、进程、线程资源使用
- 进程之间的内存不共享
- 线程之间的内存一部分共享一部分独占:
- 每一个线程都有一个自己独有的栈内存,是不共享的,各自执行各的,互不干扰,这就是多线程并发。(区分并行)
- 而一个进程的堆内存、方法区内存是该进程的所有线程共享的。
3、为什么要有线程?
- 在CPU单核时代,线程并不是同时运行,线程主要是为了提高CPU和IO设备的总和利用率,一个线程在使用CPU不用IO时另一个线程可以同时使用IO设备,而不像单线程必须等前一个线程完全执行完。
- 在如今CPU多核时代,每一个核心课运行一个线程,多个线程可以同时运行,线程提高的是CPU的利用率,多个线程同时执行可以让CPU的多个核心同时被得到利用。
4、Java如何使用多线程?分析start和run
有三种方式:
1、编写一个类继承Thread类,并重写父类run方法。
public class Main{
public static void main(String[] args) {
//这里是main方法,属于主线程,在主栈中运行
MyThread mythread = new MyThread();
/*****************
* 启动线程:
* start方法作用:1、启动一个分支线程,2、开辟一个新的分支栈空间,然后这段代码几乎瞬间就结束了
* 这段代码主要是为了开辟一个新的栈空间,开辟完就瞬间结束了,线程就启动成了。
* 启动完线程后线程会自动调用run方法,就类似主线程启动后会自动调用程序入口main方法。
* run方法与main方法都在各自的栈底部,他们两个是平级的。
*------------------------------------------------
*不使用start方法,直接使用run方法会怎样?
*前面就说了start方法就两个作用:启动线程+开辟新的栈空间,如果不用这个方法直接用调用run方法就
*只是一个在主线程内和以前一样的对象方法调用而已,还是单线程并没有产生一个新的线程。
*/
mythread.start();
//这段代码在主线程中执行
for(int i = 0; i < 10; i++){
System.out.println("主线程:"+i);
}
}
}
class MyThread extends Thread{
@Override
public void run(){
for(int i = 0; i < 10; i++){
System.out.println("分支线程:"+i);
}
}
}
2、利用 Thread(参数:可运行对象) 这个构造方法 new 一个线程Thread对象,一个类实现了Runnable接口就表示这个类是可运行的,如果你传一个不可运行的对象给Thread那这个线程就没意义了。
public class Main{
public static void main(String[] args) {
//创建线程对象
Thread mythread = new Thread(new MyRunnable());
mythread.start();
//这段代码在主线程中执行
for(int i = 0; i < 10; i++){
System.out.println("主线程:"+i);
}
}
}
//这并不是一个线程类,只是表示你这个类是可运行的
class MyRunnable implements Runnable{
@Override
public void run(){
for(int i = 0; i < 10; i++){
System.out.println("分支线程:"+i);
}
}
}
3、实现Callable接口
Runnable
自 Java 1.0 以来一直存在,但Callable
仅在 Java 1.5 中引入,目的就是为了来处理Runnable
不支持的用例。Runnable
接口不会返回结果或抛出检查异常,但是**Callable
接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 **Runnable
接口,这样代码看起来会更加简洁。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DLsBjb7y-1617939933879)(C:\Users\91051\AppData\Roaming\Typora\typora-user-images\image-20210316125824792.png)]
总结:
- 一般使用第二种,因为面向接口编程,而且如果使用继承实现了由于Java单继承你就无法继承其它类了。
- 两种方式都要重写run方法,但无论接口还是父类的run方法都没有抛出异常,所有我们在重写run方法是不能throws抛出异常,必须自己try-catch处理。
5、线程的生命周期
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4 节)。
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4 节):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wcgnVaS8-1617939933887)(C:\Users\91051\AppData\Roaming\Typora\typora-user-images\image-20210315112023542.png)]
订正(来自issue736):原图中 wait到 runnable状态的转换中,
join
实际上是Thread
类的方法,但这里写成了Object
。
由上图可以看出:线程创建之后它将处于 NEW(新建) 状态,调用 start()
方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。
操作系统隐藏 Java 虚拟机(JVM)中的 READY 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:HowToDoInJava:Java Thread Life Cycle and Thread States),所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。
当线程执行 wait()
方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)
方法或 wait(long millis)
方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnable 的run()
方法之后将会进入到 TERMINATED(终止) 状态。
6、线程的基本方法使用
1、获取当前线程对象、获取与设置线程名字
/***************
Thread currentThread = Thread.currentThread();
String currentThreadName = currentThread.getName()
currentThread.setName("Thread-01")
********************/
class MyRunnable implements Runnable{
@Override
public void run(){
for(int i = 0; i < 10; i++){
System.out.println("线程:"+Thread.currentThread().getName+"执行:"+i);
}
}
}
2、sleep方法 :public static native void sleep(long millis) throws InterruptedException;
Thread.sleep(毫秒)
1、让当前线程进入休眠状态,线程进入超时等待状态(不会释放锁),到了时间重新进入运行状态(等待、运行)。
2、sleep无论是父类直接调用还是子类调用作用都是一样:用子线程对象调用sleep方法并不代表就让人该子线程休眠
还是让“当前线程进入休眠状态”
interrupt() : 中断线程睡眠(是中断睡眠,不是中断线程)
myThread.interrupt();
依靠的是java的异常处理机制,会让对应线程的sleep报异常退出方法执行或进入catch语句
3、stop:中断线程执行
myThread.stop();
直接中断线程执行,已过时,因为是直接将线程中断,容易丢失数据
添加判断变量中断线程执行
public class Main{
public static void main(String[] args) {
MyRunnable myRunnab = new MyRunnable();
//创建线程对象
Thread mythread = new Thread(myRunnab);
mythread.start();
//这段代码在主线程中执行
for(int i = 0; i < 10; i++){
System.out.println("主线程:"+i);
//主线程执行到i=5时就将分支线程mythread终止
if(i == 5){
myRunnab.setIsStop(true);
}
}
}
}
class MyRunnable implements Runnable{
private Boolean isStop = false;
//isStop的get、set...
public void setIsStop(Boolean isStop){
this.isStop = isStop;
}
@Override
public void run(){
if(!isStop){
for(int i = 0; i < 10; i++){
System.out.println("线程:"+Thread.currentThread().getName()+"执行:"+i);
}
}else{
//在结束前还有什么数据没有保存的或需要做些其它什么操作
// ...
return ;//return就结束了,
}
}
}
7、线程同步机制
使用synchronized,线程排队执行
使用方式:
1、修饰实例、静态方法
synchornized void method(){
}
synchornized static void method(){
}
-
修饰实例方法是对“对象”加锁,每一个对象都有且仅有一把锁,某个线程想访问一个对象的这个synchornized实例方法,就必须获得这个对象的锁(如果其它线程在访问就表示锁已经被其它线程拿住了,本线程就进入“阻塞状态”,必须等它执行完释放锁后本线程才能执行)。
-
修饰静态方法是对“类”加锁,一个类只有一把锁,与上面对象加锁类似,访问前必须拿到类的锁。
注意:“对象锁”与“类锁”互不干扰,如果一个线程 A 调用一个实例对象的非静态
synchronized
方法,而线程 B 需要调用这个实例对象所属类的静态synchronized
方法,是允许的,不会发生互斥现象
2、修饰代码块
synchornized(this|object){
代码块
}
指定加锁对象,对给定对象/类加锁。synchronized(this|object)
表示进入同步代码库前要获得给定对象的锁。synchronized(类.class)
表示进入同步代码前要获得 当前 class 的锁
注意:尽量不要使用 synchronized(String a)
因为 JVM 中,字符串常量池具有缓存功能!
8、死锁和守护线程
死锁一种情况:线程A拿着锁1请求锁2,线程B拿着锁2请求suo1…
public class Main{
public static void main(String args[]){
Object o1 = new Object();
Object o2 = new Object();
Thread t1 = new Thread1(o1, o2);
Thread t2 = new Thread2(o1, o2);
t1.start();
t2.start();
}
}
class Thread1 extends Thread{
private Object o1;
private Object o2;
public Thread1(Object object1, Object object2){
o1 = object1;
o2 = object2;
}
@Override
public void run(){
//线程A拿着锁1请求锁2
synchronized(o1){
try {
Thread.sleep(1000);
System.out.println("拿到锁1,等待锁2...");
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(o2){
}
}
}
}
class Thread2 extends Thread{
private Object o1;
private Object o2;
public Thread2(Object object1, Object object2){
o1 = object1;
o2 = object2;
}
@Override
public void run(){
//线程B拿着锁2请求suo1
synchronized (o2){
System.out.println("拿到锁2,等待锁1...");
synchronized(o1){
}
}
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XFqyHg7X-1617939933891)(C:\Users\91051\AppData\Roaming\Typora\typora-user-images\image-20210316114947845.png)]
守护线程
Java中线程分为两种:用户线程和守护线程(后台线程)。
-
Java最代表性的守护线程:GC垃圾回收线程。一般守护线程是一个死循环,所有用户线程结束,守护线程自动结束
-
主线程main方法也是用户线程。
9、wait()和notify()
方法概述
wait与notify方法不是属于线程类Thread的,是属于Object类自带的,每一个java对象都有这两个方法
作用
Object o = new Object();
o.wait();
/**
*表示让在o对象上活动的线程进入无限等待状态,并释放o对象的锁(sleep不会释放锁),直到调用o.notify()被唤醒
*/
o.notify()//唤醒在o对象上活动的一个线程(随机一个),不会释放之前已经占有着的o对象的锁
生产者与消费者
import java.util.ArrayList;
import java.util.List;
public class Main{
public static void main(String args[]){
List<Integer> list = new ArrayList<>();
Thread t1 = new Thread1(list);
Thread t2 = new Thread2(list);
t1.start();
t2.start();
}
}
class Thread1 extends Thread{
private List<Integer> list;
public Thread1(List<Integer> list){
this.list = list;
}
@Override
public void run(){
while(true){
synchronized (list){
if(list.size() == 0) {
try {
System.out.println("list为空,等待生产...");
list.wait();
System.out.println("list开始消费...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// }else {
// System.out.println("消费掉:"+list.size());
// list.remove(list.size()-1);
// list.notifyAll();
// }
System.out.println("消费掉:"+list.size());
list.remove(list.size()-1);
list.notifyAll();
}
}
}
}
class Thread2 extends Thread{
private List<Integer> list;
public Thread2(List<Integer> list){
this.list = list;
}
@Override
public void run(){
while(true){
synchronized (list){
if(list.size() >= 5) {
try {
System.out.println("list已满,等待消费...");
list.wait();
System.out.println("list开始生产...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// }else{
// System.out.println("list生产:"+(list.size()+1));
// list.add(list.size());
// list.notifyAll();
// }
System.out.println("list生产:"+(list.size()+1));
list.add(list.size());
list.notifyAll();
}
}
}
}
注意:唤醒并且被执行的线程是从上次阻塞的位置从下开始运行,也就是从wait()方法后开始执行。