1、进程与线程的关系
进程:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。
线程:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
1.1 多线程 执行原理 ( 并发执行原理 )
CPU核心: 每一个计算机中CPU核心, 就相当于人的一个大脑, 可以执行一件事情.
时间片: 将CPU未来可以用于执行的时间 ,碎片化, 拆分成一个个的小的时间片段. 每一个时间片段的大小 可以都不足一毫秒.
程序在争抢到时间片段后, 会进行执行 .
2、Java 实现线程三种方式
方式1. 继承Thread类
步骤:
1. 编写一个类, 继承Thread类
2. 重写父类的 run 方法 , 在此方法中调用执行的代码 就是在新的线程中执行的代码.
3. 创建此类的对象, 并调用start方法, 来启动此线程 ;
案例:
public class Demo {
public static void main(String[] args) {
MyThread m1 = new MyThread();
m1.start();
for(int i=0;i<100;i++) {
System.out.println("MainThread"+i);
}
}
}
class MyThread extends Thread{
@Override
public void run() {
for(int i=0;i<100;i++) {
System.out.println("MyThread"+i);
}
}
}
方式2. 实现Runnable接口
步骤:
1. 编写一个类, 实现Runnable接口 , 实现run方法 (run方法的逻辑, 就是在新的线程中执行的)
2. 创建上述Runnable接口实现类的 对象
3. 创建Thread对象, 并在构造方法中, 第二步创建的对象
4. 调用Thread对象的start方法,启动线程
案例:
public class Demo2 {
public static void main(String[] args) {
//1. 先创建MyRunnable对象
MyRunnable m1 = new MyRunnable();
//2. 创建Thread 对象,通过构造方法, 传入MyRunnable对象
Thread t1 = new Thread(m1);
//3. 启动线程
t1.start();
for(int i=0;i<100;i++) {
System.out.println("Main"+i);
}
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
for(int i=0;i<100;i++) {
System.out.println("MyRunnable"+i);
}
}
}
方式1(继承Thread) 和 方式2(实现Runnable) 哪个方式 更好?
Thread类在设计时, 使用到了一个设计模式: 代理设计模式.
我们使用第一种方式 和 第二种方式, 其实在内部都会使用代理设计模式, 最终的结果 都是实现了Runnable接口
在Thread内部, 无论是通过第一种方式也好, 第二种方式也好, 最终的效率, 是完全一致的.
虽然效率一致, 但是两种方式在使用时 ,更建议使用 第二种方式:
原因:
使用继承的方式, 会导致我们编写的类 无法再继承其他的类 .
而使用第二种方式 , 因为是实现Runnable接口, Java允许多实现 ,所以我们的类还可以继续实现其他的接口, 或者 继承其他的类.
方式3.
使用匿名内部类的方式, 快速的编写一个Thread的子类, 然后调用start方法 启动线程
格式:
new Thread() {
@Override
public void run() {
for(int i=0;i<100;i++) {
System.out.println("T一"+i);
}
}
}.start();
我们上述的三种方式, 都编写了一个run方法.
1. 直接通过对象 .run的方式, 不是启动线程.
只有通过start调用, 才是启动新的线程路径.
2. 一个方法, 如果被某一个线程调用执行, 那么此方法本次就执行在这个线程中 .
案例:
Util u = new Util();
new Thread() {
public void run() {
u.x();
}
}.start();
new Thread() {
public void run() {
u.x();
}
}.start();
Thread 类 常用方法
- 常用的静态方法
静态方法作用于,调用的线程。
- static void sleep(int 毫秒数) ***
此方法执行时 所在的线程睡眠 , 用于降低线程的活跃度 .
- static Thread currentThread();
获取此方法执行时 所在线程的 线程对象.
- static void yield();
此方法执行时所在的线程, 让出当前时间片. 用于降低线程的活跃度
- 常用的非静态方法
- void start() ***
启动此线程所表示的线程对象, 并在新的线程中执行run方法
- void setName(String name) | String getName(); 了解
设置/获取 线程名称; 线程拥有默认的名称
- stop()
停止线程(已过时 , 不建议使用, 因为会造成固有的不安全性);
- interrupt()
中断线程!
此方法用于在某一个线程上 加入 中断标记.
这个线程被加入标记后, 它正在执行的逻辑,如果触发一些特殊的方法, 会出现异常 !
通常此操作用于通知线程停止 . 具体是否停止, 由线程自身决定.
- setDaemon(boolean flag) 了解
传入true设置线程为守护线程 ! 默认一个线程为用户线程 .
- setPriority(int 优先级) 了解
线程优先级: 取值1-10 , 表示线程的10个优先等级. 10最高 1最低
优先级的作用是: 让线程更有可能争抢到时间片 .
系统提供了建议使用的三种值:
1: 较低的优先级
5: 中等的优先级
10: 较高的优先级
注意: 调整一个线程的优先级, 并不能保证一个线程绝对能争抢到时间片, 只是几率大了一丢丢 !
1和10的区别大概可以理解为: 50% 和51%
线程默认优先级
一个线程的默认优先级 与 启动它的线程完全一致 ;main线程的优先级为5
线程 共分为两类
1. 用户线程
通常情况下, 我们自己开启的线程都是用户线程, 所有的用户线程都是一条单独的执行路径, 一个软件的所有用户线程执行完毕, 程序会死亡 .
2. 守护线程
程序中存在一种特殊的线程, 叫做守护线程, 可以理解为 : 守护用户线程 .
当程序中所有的用户线程都死亡了. 守护线程无论是否执行完毕, 都会自动死亡.
Java中的垃圾回收器 GC 其实就是守护线程.
Java中存在单线程程序吗?
答: 不存在的 . Java在启动时必然存在的有两个线程:
1. main线程
2. GC垃圾回收器
3、线程的生命周期
3.1.创建
public class MyThread extends Thread{
@Override
public void run() {
//...
}
}
//新建就是new出对象
MyThread thread = new MyThread();
当程序使用new关键字创建了一个线程之后,该线程就处于一个新建状态(初始状态),此时它和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,并初始化了其成员变量值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。
3.2.就绪
当线程对象调用了Thread.start()方法之后,该线程处于就绪状态。
Java虚拟机 会为其创建方法调用栈和程序计数器,处于这个状态的线程并没有开始运行,它只是表示该线程可以运行了。从start()源码中看出,start后添加到了线程列表中,接着在native层添加到VM中,至于该线程何时开始运行,取决于JVM里线程调度器的调度(如果OS调度选中了,就会进入到运行状态)。
3.3.运行
当线程对象调用了Thread.start()方法之后,该线程处于就绪状态。
添加到了线程列表中,如果OS调度选中了,就会进入到运行状态
3.4.阻塞
阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况大概三种:
- 1、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
- 2、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
- 3、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)。
- 线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
- 线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。唤醒线程后,就转为就绪(Runnable)状态。
- 线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
- 线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
- 线程I/O:线程执行某些IO操作,因为等待相关的资源而进入了阻塞状态。比如说监听system.in,但是尚且没有收到键盘的输入,则进入阻塞状态。
- 线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意性的,并在对实现做出决定时发生。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。
3.5.死亡
线程会以以下三种方式之一结束,结束后就处于死亡状态:
- run()方法执行完成,线程正常结束。
- 线程抛出一个未捕获的Exception或Error。
- 直接调用该线程的stop()方法来结束该线程——该方法会产生固有的不安全性,不推荐.
4、线程同步
同步即排队 . 异步即并行.
线程安全问题:
当多个线程同时操作一段数据时,容易发生问题。
同步就是线程安全, 效率较低! 线程安全可以有效的结果 临界资源出错问题 !
异步表示线程非安全, 效率高
4.1、线程同步的两种实现方式
两种方式 都是通过 加锁 来完成同步操作的.
记住一点: 在同步代码块时 我们手动指定锁对象时 . 多个线程必须使用同一个锁 ,才可以完成同步操作 !
同步代码块
格式:
synchronized(Object 锁对象){
}
同步方法
非静态的同步方法的锁对象为this
静态方法的锁对象 , 是我们的类信息对象 (类名.class).
格式
权限修饰符 synchronized 返回值声明 方法名(形式参数列表){
}
4.2、同步方法 与 同步代码块, 何时使用, 选择哪个?
同步代码块 粒度更小 . 更灵活 !
同步方法的操作 最小单位就是一个方法所有的 代码 .
同步方法因为默认使用了this作为锁对象. 所以它存在一个特殊的操作:
同一个对象中的 所有同步方法, 均使用this这一把锁, 所以可以达到 多方法同时上锁的需求 !
案例:
public class Demo {
@Test
public void haha() throws Exception {
for(int i=0;i<2000;i++) {
MyThread t1 = new MyThread();
t1.start();
}
}
}
class MyThread extends Thread{
static int count = 1999;
static Object o = new Object();
@Override
public void run() {
//同步代码块
synchronized(o) {
//1. 判断余票是否大于0
if(count>0) {
//余票充足
System.out.println(getName()+"余票充足:"+count);
System.out.println(getName()+"正在出票...");
count--;
System.out.println(getName()+"出票成功, 余票:"+count);
}else {
//余票不足
System.out.println(getName()+"余票不足:"+count);
}
}
}
}
4.3线程死锁
案例:
public class A {
public synchronized void a1(B b) {
System.out.println("a1在执行");
b.b2();
}
public synchronized void a2() {
System.out.println("a2在执行");
}
}
public class B {
public synchronized void b1(A a) {
System.out.println("b1在执行");
a.a2();
}
public synchronized void b2() {
System.out.println("b2在执行");
}
}
public class Demo {
public static void main(String[] args) {
A a = new A();
B b = new B();
Thread t1 = new Thread() {
public void run() {
a.a1(b);
}
};
Thread t2 = new Thread() {
public void run() {
b.b1(a);
}
};
t1.start();
t2.start();
}
}
当两个或多个线程 互相加锁时 , 就形成了死锁 .
死锁应尽可能的避免 , 避免死锁应遵循如下三个原则:
1. 顺序上锁
2. 反向解锁
3. 不要回头
4.4、线程池 Executors
池: 表示容器, 为了重复利用而设计.
线程池执行的流程:
池中会缓存一些空闲线程 ,
当需要多线程执行某流程时 , 会去池中判断 是否还存在空闲线程 .
如果存在空闲线程, 则使用空闲线程进行操作 .
如果不存在空闲线程, 且池未满的情况下, 会创建空闲线程存储到池中, 然后使用.
如果不存在空闲线程, 且池已满的情况下, 会排队等待线程空闲.
池不要用,池不符合规范。 设计有小缺陷。
阿里巴巴编程规约描述:这四种线程池有缺陷,不要用。要用自己造。
4.5、Java中的四种线程池 . ExecutorService
1. 缓存线程池
/**
* 缓存线程池.
* (长度无限制)
* 执行流程:
* 1. 判断线程池是否存在空闲线程
* 2. 存在则使用
* 3. 不存在,则创建线程 并放入线程池, 然后使用
*/
ExecutorService service = Executors.newCachedThreadPool();
//向线程池中 加入 新的任务
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
2. 定长线程池
/**
* 定长线程池.
* (长度是指定的数值)
* 执行流程:
* 1. 判断线程池是否存在空闲线程
* 2. 存在则使用
* 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
* 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
*/
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
3. 单线程线程池
效果与定长线程池 创建时传入数值1 效果一致.
/**
* 单线程线程池.
* 执行流程:
* 1. 判断线程池 的那个线程 是否空闲
* 2. 空闲则使用
* 4. 不空闲,则等待 池中的单个线程空闲后 使用
*/
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:"+Thread.currentThread().getName());
}
});
4. 周期性任务定长线程池
public static void main(String[] args) {
/**
* 周期任务 定长线程池.
* 执行流程:
* 1. 判断线程池是否存在空闲线程
* 2. 存在则使用
* 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
* 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
* 周期性任务执行时:
* 定时执行, 当某个时机触发时, 自动执行某任务 .
*/
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
/**
* 定时执行
* 参数1. runnable类型的任务
* 参数2. 时长数字
* 参数3. 时长数字的单位
*/
service.schedule(new Runnable() {
@Override
public void run() {
System.out.println("石鹏魏洋相视一笑~ 嘿嘿嘿");
}
},5,TimeUnit.SECONDS);
*/
/**
* 周期执行
* 参数1. runnable类型的任务
* 参数2. 时长数字(延迟执行的时长)
* 参数3. 周期时长(每次执行的间隔时间)
* 参数4. 时长数字的单位
*/
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("石鹏魏洋相视一笑~ 嘿嘿嘿");
}
},5,2,TimeUnit.SECONDS);
}
1、Java存在单线程程序吗?
答案:不存在,一个Java程序运行时,必然会执行的有两个线程:main线程,GC线程