java多线程
1. 进程
当程序进入内存运行时,就启动了一个进程,即进程是处于运行过程的程序。
操作系统会给每个进程分配独立的内存等资源,即进程时操作系统资源分配,调度和管理的最小单位。
进程包含三个特性:
- 独立性:每个进程拥有自己的地址空间,不经过进程的允许,一个用户进程不可直接方位其他进程的地址空间。
- 动态性:程序只是静态指令的集合,而进程是一个正在系统运行的活动指令的集合。进程加入了时间的概念,拥有自己的生命周期,这是程序不具备的。
- 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会相互影响。
- 并行:多条指令在多个处理器同时执行。
- 并发:多条指令在单个处理器切换执行。
2. 线程
多进程扩展了多线程的概念,使得一个进程可以同时
并发
处理多个任务,线程也被称为轻量级线程。当进程被初始化后,主线程就被创建了。对于Java来说,main线程就是主线程,我们可以在main函数创建多条顺序执行路径,这些独立执行的路径都是线程。
由于线程间的通信是在同一个地址空间上进行的,不需要额外的同通信机制,使得通信更简洁,速度快。
CPU可以在不同的进程中轮换,进程又可以在不同的线程之间轮换。故线程是CPU执行和调度的最小单元。
3. 线程的创建
3.1 继承Thread类
- 子类重写run方法
- 主线程调用start方法
public class TestThread {
public static void main(String[] args) {
// 在主线程 打印1-100 其余两个线程 打印 1-100 同时进行
MyThread thread1 = new MyThread();
thread1.setName("thread-1");
MyThread thread2 = new MyThread();
thread2.setName("thread-2");
Thread.currentThread().setName("thread-3");
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName()+">>>> "+i);
}
thread1.start();
thread2.start();
}
static class MyThread extends Thread{
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName()+">>>> "+i);
}
}
}
}
3.2 实现Runnable接口
-
实现Runnable接口,实现run方法
-
创建Thread类,Runnable接口作为参数传递
相同方法传递相同
-
调用start方法
public class TestRunnable {
public static void main(String[] args) {
Runnable runnable1 = () -> {
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + ">>>>> " + i);
}
};
new Thread(runnable1).start();
new Thread(runnable1).start();
}
}
实现只有三个窗口有权限卖票:
使用Runnable接口:
public static void main(String[] args) {
SellTickets sellTickets = new SellTickets();
new Thread(sellTickets).start();
new Thread(sellTickets).start();
new Thread(sellTickets).start();
}
static class SellTickets implements Runnable{
private int ticket=100;
@Override
public void run() {
while (true){
if (ticket<=0){ // 每次卖票操作先判断
System.out.println("ticket is empty~~~");
return;
}
/*卖票操作 */
System.out.println(Thread.currentThread().getName()+" remaining "+ --ticket);
}
}
}
- Runnable 接口避免了单继承的局限性。
- 适合处理共享资源的情况。
4. 线程的生命周期
在jdk5 之前一个完整的线程的生命周期一般经历五种状态,从操作系统层面来描述:
新建、就绪、运行、阻塞、死亡。
- 线程生命周期流程图
4. 1 新建
当一个子类或其子类对象被创建时,新生的线程对象就处于新建状态,此时没有任何动态特征,和一般对象相同。
4. 2 就绪
当线程对象调用了start()方法,线程由创建转化为就绪状态,已经具备了运行的条件,随时可以调度,调度时间由JVM操控。
4. 3 运行
处于就绪状态的线程对象获取cpu,开始执行run()方法。多核cpu的并行,单核cpu并发。
4. 4 阻塞
进入阻塞的原因:
- 线程调用sleep() 方法,主动放弃CPU资源。
- 线程调用阻塞式IO方法,在方法返回之前,该线程处于阻塞。
- 线程试图获取同步监视器,同步监视器被其他线程持有。
- 同步监视器调用wait()方法,让它等待某个通知。
- 运行阶段遇到某个线程的加塞。
- 线程被调用的supend方法挂起。
阻塞结束情况:
- 线程sleep时间结束。
- 线程调用阻塞式IO已返回。
- 线程获取同步监视器。
- 线程得到通知。
- 加塞的线程结束。
- 被挂起的线程被调用了resume方法。
4. 5 死亡
- run方法执行结束。
- 抛出一个未捕获异常或错误。
- 调用该线程的stop方法。
- 使用isAlive()方法进行判断是否处于就绪、运行、阻塞状态。
5. Thread类
5. 1 构造和基本方法
构造器:
- Thread() 默认构造,线程名默认。
- Thread(String name) 指定线程名。
- Thread(Runnable r,String name) 指定线程目标对象,执行线程名。
方法:
- run() 线程执行的方法体。
- start() 启动线程。
练习: 实现Runnable接口实现打印奇数,继承Thread类打印偶数。
package indi.jihad.a7;
public class TestA {
public static void main(String[] args) {
new Thread(new MyRunnable(),"奇数线程:").start();
new MyThread("偶数线程:").start();
}
static class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 1; i <=100; i++) {
if (i%2!=0){
System.out.println(Thread.currentThread().getName()+">>>> "+i);
}
}
}
}
static class MyThread extends Thread{
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 1; i <=100; i++) {
if (i%2==0){
System.out.println(Thread.currentThread().getName()+">>>> "+i);
}
}
}
}
}
5. 2 线程信息
- static Thread Thread.currentThread() 静态方法,总是返回当前线程对象。
- boolean isAlive() 线程是否存活。
- final int getPriority() 返回线程优先级。
- inal int setPriority(int priority) 设置线程优先级。
练习:设置卖票窗口的优先级:
public static void main(String[] args) {
SellTickets sellTickets = new SellTickets();
Thread thread1 = new Thread(sellTickets);
thread1.setPriority(Thread.MAX_PRIORITY);
Thread thread2 = new Thread(sellTickets);
thread2.setPriority(Thread.NORM_PRIORITY);
Thread thread3 = new Thread(sellTickets);
thread3.setPriority(Thread.MIN_PRIORITY);
thread1.start();
thread2.start();
thread3.start();
}
static class SellTickets implements Runnable{
private int ticket=100;
@Override
public void run() {
while (true){
if (ticket<=0){
System.out.println("ticket is empty~~~");
return;
}
// 卖票操作
System.out.println(Thread.currentThread().getName()+" remaining "+ --ticket);
}
}
}
5. 3 线程的控制
- static void sleep(long millis) 在指定毫秒数让当前线程休眠。
- static void yield() 当前执行的线程暂停,不会阻塞,将该线程转入就绪。
- final void join() 加塞,直至该线程结束,另一线程在执行。
- final void stop() 强迫线程停止。
- final void respend() 挂起当前线程。
- final void resume() 重新开始执行挂起的线程。
- void interrupt() 中断线程。
- static boolean interrupted() 测试当前线程是否中断,中断状态将被清除,连续两次调用返回false。
- void setDeamon(boolean on) 设置为守护线程,在线程启动前设置。
练习1: 倒计时—sleep
public static void main(String[] args) throws InterruptedException {
for (int i = 5; i >0; i--) {
System.out.println("the "+i+" s losing");
Thread.sleep(1000);
}
System.out.println("end.....");
}
练习2:线程让步—yield
yield方法 暂停一下,cpu重新调度,每个线程争夺资源。
public static void main(String[] args) throws InterruptedException {
Runnable runnable=()->{
for (int i=0;i<=5;i++){
System.out.println(Thread.currentThread().getName()+": >>>> " +i);
Thread.yield();
}
};
Thread thread1 = new Thread(runnable,"hight");
thread1.setPriority(Thread.MAX_PRIORITY);
Thread thread2 = new Thread(runnable,"low");
thread2.setPriority(Thread.MIN_PRIORITY);
thread1.start();
thread2.start();
}
练习3:龟兔赛跑— join
- 当某个线程的线程体调用另一个线程的join方法,该线程将被阻塞,直到join进来的线程执行完(join不限时加塞),或阻塞一段时间(join(millis)限时加塞)后,它才能继续执行。
案例需求:
- 假设跑道长度为30m。
- 兔子的速度为10m/s,兔子每次跑完10m,休息10s。
- 乌龟的速度为1m/s, 乌龟每次跑完10m,休息1s。
- 两个线程结束,主线程公布结果。
package indi.jihad.a8;
import lombok.Data;
public class TestRace {
public static void main(String[] args) {
Racer racer1 = new Racer("兔子", 100, 10000, 30);
Racer racer2 = new Racer("乌龟", 1000, 1000, 30);
racer1.start();
racer2.start();
// 两线程在主线程之前执行完
try{
racer1.join();// 只有线程1 线程2 执行
racer2.join();// 只有 线程2 执行
}catch (Exception e){
}
// 只有主线程执行
String winner=racer1.totalTime<racer2.getTotalTime()?racer1.getRaceName(): racer2.getRaceName();
System.out.println("winner is "+winner);
}
@Data
static class Racer extends Thread{
private String raceName;
private long runTime;
private long restTime;
private long distance;
private long totalTime;
private boolean finished;
public Racer(String raceName, long runTime, long restTime, long distance) {
this.raceName = raceName;
this.runTime = runTime;
this.restTime = restTime;
this.distance = distance;
}
@Override
public void run() {
long sum=0;
long startTime=System.currentTimeMillis();
while (sum<distance){
System.out.println(raceName+" is running......");
try{
Thread.sleep(runTime);// 每秒跑步时间
}catch (Exception e){
System.out.println(raceName+" is hurted");
return;
}
sum++;//距离+1 跑了1m
if(sum%10==0&&sum<distance){
try{
Thread.sleep(restTime);// 休息时间
}catch (Exception e){
System.out.println(raceName+" is hurted");
return;
}
}
}
long endTime=System.currentTimeMillis();
totalTime=(endTime-startTime);
System.out.println(raceName+" >>>>"+totalTime+" ms");
finished=true;
}
}
}
练习4 :守护线程 —daemon
- 守护线程:在后台运行,为其他线程提供服务.比如JVM的垃圾回收线程。
- 特点:非守护线程全部死亡,守护线程跟随死亡。
- 主线程 打印1-100 守护线程打印 i am daemon
package indi.jihad.a8;
public class TestDaemon {
public static void main(String[] args) {
Thread daemonThread = new Thread(() -> {
while (true){
System.out.println("i am daemon");
}
});
daemonThread.setDaemon(true);
daemonThread.start();
for (int i = 1; i <=100 ; i++) {
System.out.println("主线程>>>>>>>>"+ i);
}
}
}
- 输出片段
练习5: 中断线程—interrupt
interrupt调用遇上以下方法抛出异常 InterruptedExpection:
- Object:wait、wait(long)、wait(long,int)
- Thread:join、sleep。
- 可以实现线程停止。完成龟兔赛跑:有一方到达终点即比赛就结束。
6. 线程同步
6. 1 线程安全问题
线程安全问题都是由共享变量引起的,共享变量一般都是某个类的静态变量,或者因为多个线程使用同一个对象的实例变量。方法的局部变量是不可能成为共享变量。
- 只有读操作,没有修改操作,一般来说,该共享变量是线程安全的。
- 如果多个线程同时执行写操作,就要考虑线程安全问题。
线程安全示例:窗口卖票:
package indi.jihad.a8;
public class TestSellTicketWithThreeWindows {
public static void main(String[] args) {
new SellTickets().start();
new SellTickets().start();
new SellTickets().start();
}
static class SellTickets extends Thread{
private static int ticket=100;
@Override
public void run() {
while (ticket>0){
--ticket;//卖票
// 通知
System.out.println(Thread.currentThread().getName()+" remaining "+ ticket);
}
}
}
}
- 通知和卖票会错乱,出现错票、多票等线程安全问题。
6. 2 同步代码块
- 6.1 解决方案
卖票线程安全问题:
- 通知可能会是另一个线程的结果。(同步代码块绑定)
- 当ticket<=0 时其他线程已进入while判断。(同步代码块加入if判断)
package indi.jihad.a8;
public class TestSellTicketWithThreeWindows {
public static void main(String[] args) {
new SellTickets().start();
new SellTickets().start();
new SellTickets().start();
}
static class SellTickets extends Thread{
private static int ticket=100;
@Override
public void run() {
while (ticket>0){
// 多个线程进入
synchronized (this.getClass()){
if (ticket>0){// 单个进程进入判断是否有票
--ticket;//卖票
// 通知
System.out.println(Thread.currentThread().getName()+" remaining "+ ticket);
}
}
}
}
}
}
- 使用synchronized(锁) 让获取锁的单个线程独立执行,其余线程等待获取锁。
- 锁要唯一,一般选取当前对象的Class对象。
6. 3 同步方法
静态方法使用的锁为:当前类的Class对象。
非静态方法使用的锁为:当前this对象。
- 修改6. 2 代码
package indi.jihad.a8;
public class TestSellTicketWithThreeWindows {
public static void main(String[] args) {
new SellTickets().start();
new SellTickets().start();
new SellTickets().start();
}
static class SellTickets extends Thread{
private static int ticket=100;
@Override
public void run() {
while (ticket>0){
sellAndAdviceTicket();
}
}
public static synchronized void sellAndAdviceTicket(){
if (ticket>0){
--ticket;//卖票
// 通知
System.out.println(Thread.currentThread().getName()+" remaining "+ ticket);
}
}
}
}
6. 4 释放锁操作
释放锁的操作:
- 当前同步代码块执行结束。
- 遇到break (外循环) 和return。
- 出现异常未捕获。
- 执行锁对象的wait方法,相乘被挂起。
不会释放锁的操作:
- 程序调用Thread.sleep、Thread.yield方法。
- 其他线程调用了该线程的suspend将该线程挂起。
6. 5 死锁
当不同的线程分别锁住对方需要的同步监视器对象不释放时,就会形成死锁。
一旦产生死锁,程序不会报任何异常,只是所有程序处于阻塞状态。
死锁示例
package indi.jihad.a8;
public class TestDeadLock {
public static void main(String[] args) {
String goods="商品";
Double money=10.0;
new Thread(new Customer(goods,money)).start();
new Thread(new Owner(goods,money)).start();
}
static class Customer implements Runnable{
private String goods;
private Double money;
public Customer(String goods, Double money) {
this.goods = goods;
this.money = money;
}
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (money){
System.out.println("等待卖家发货.....");
synchronized (goods){
System.out.println("顾客已付钱....");
}
}
}
}
static class Owner implements Runnable{
private String goods;
private Double money;
public Owner(String goods, Double money) {
this.goods = goods;
this.money = money;
}
@Override
public void run() {
synchronized (goods){
System.out.println("等待顾客付钱.....");
try {
Thread. sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (money){
System.out.println("商家发货....");
}
}
}
}
}
7. 等待唤醒机制
7. 1 基本方法
Object提供了三个方法(使用在同步代码块中,由锁调用):
- wait 释放锁 等待被唤醒
- notify随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会
- notifyAll会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会
- 线程:交替打印1-100
public class Test01 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
new Thread(myThread,"1>>>>").start();
new Thread(myThread,"2>>>>").start();
}
static class MyThread implements Runnable{
private int num=1;
@Override
public void run() {
while (num<=100){
synchronized (this){
this.notify();
if (num<=100){
System.out.println(Thread.currentThread().getName()+" >>>> "+num);
num++;
}
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
7.2 生产者消费者问题
生产者与消费者问题也称有限缓冲问题。是多线程同步的问题的经典案例。
生产者在缓冲区生成一定量的数据,于此同时消费者在缓冲区消耗数据。
生产者和消费者隐含两个线程问题:
- 线程安全问题,消费者和生产者存在共享的数据,需要进行同步机制解决。
- 线程协调工作问题:这个问题需要等待唤醒机制解决。
- 关键代码
public synchronized void produceProduct() {
if (productionNum < 20) {
productionNum++;
notify();// 唤醒消费线程
System.out.println(Thread.currentThread().getName()
+ "is producing the " +
productionNum + " production");
} else {
try {
wait();// 等于20 个等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void consumerProduct(){
if (productionNum > 0) {
System.out.println(Thread.currentThread().getName()
+ "is consuming the " +
productionNum + " production");
productionNum--;
notify();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
8. JDK5 线程特性
8. 1 Callable接口
相比Runnable ,Callable优势
- call方法可以有返回值。
- 方法可以抛出异常。
- 支持泛型返回值。
- 借助FutureTask类,获取返回结果。
public class TestCallable {
public static void main(String[] args) {
MyCallable callable = new MyCallable();// 创建实现类
FutureTask<Integer> task = new FutureTask<Integer>(callable);// 创建代理FutureTask
Thread thread = new Thread(task);// 创建线程
thread.start();// 启动线程
try {
Integer sum = task.get();// 获取返回值
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
static class MyCallable implements Callable<Integer> {
private Integer sum=0;
@Override
public Integer call() throws Exception {
for (int i = 1; i <= 100; i++) {
if (i%2==0){
System.out.println(Thread.currentThread().getName()+">>>> "+i);
sum+=i;
}
}
return sum;
}
}
}
8. 2 线程池
背景: 经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好 多个线程,放入线程池中,使用时直接获取,使用完放回池中,可以避免频繁的创建和销毁,实现重复利用。
好处:
- 提高响应速度。
- 降低资源消耗。
- 便于线程管理。
public class TestThreadPool {
public static void main(String[] args) {
ThreadPoolExecutor
service = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);// 设置确定线程数的线程池
service.setCorePoolSize(15);// 设置池中核心连接数
service.execute(()->{// 执行Runnable接口函数
for (int i=1;i<=100;i++){
if(i%2==0) System.out.println(Thread.currentThread().getName()+">>> "+i);
}
});
service.shutdown();// 关闭线程池
}
}