Java多线程-初步学习笔记

多线程

主要思路:以多个场景,逐步引导,从"为什么、是什么、怎么做"来理解、学习Java线程

场景1:初识线程概念

用QQ的时候,跟一个人互相发消息,同时也能收到别人发来的消息

为什么需要线程这种概念?

  • 个人理解:

如果按照以前写一个main方法的思路,分别写发消息、收消息的方法后,就要以一定时间间隔循环调用两个方法,比较麻烦,且也不能实现同时、并行。

但是,当引入线程的概念后,可以把发消息、收消息方法独立开来:

线程1等待自己输入消息、发出消息,

线程2等待其他人发来消息,接收到了就推给view层。

  • 并行并发扩展:

看上去线程1、2是“同时”运行的,这个“同时”分2种情况:并行,并发。

  1. 当在多核cpu的计算机上为线程1、2指定不同cpu时(SetThreadAffinityMask),这两条线程就是并行

  2. 当不指定cpu时,大多数情况同一个进程的多线程抢占一个cpu核心,分时间片并发执行。

JAVA中的线程是什么?

从场景1使用QQ的过程中,不难看出,线程是Java可以“同时运行”的对象,在想要同时实现几个功能时使用。

主线程是进程默认启动的,其他线程是程序员创建的。

多线程共用进程的堆内存(主要放对象实例),独享栈内存(放指令、操作数)中自己的那部分。

怎么实现最简单的多线程?

虽然Thread是线程,但是看源码,Thread实现了Runnable接口,那先从Runnable接口讲起。

Runnable接口

源码很简单,只有一个抽象方法run

  • 源码注释:

当一个实现了Runnable接口的对象(可不就是Thread类的对象嘛),创建(new)、启动(start)了一条线程,它的run()方法就会自动地在一个单独的(separated)线程中被调用。这个run方法可以执行任何操作。

  • 也就是说:

我们创建的Thread子类的对象不用显示地调用run方法,创建new、启动start线程对象后,就会自动调用run方法

  • 实验

MyThread.java

public class MyThread extends Thread{
		// 因为Thread类实现了run方法,Thread子类可以重写
    @Override
    public void run() {
        System.out.println("执行run了");
    }
}

Demo1.java

public class Demo1 {
    public static void main(String[] args) {
        Thread myThread = new MyThread();// 线程对象
        System.out.println("this is the main thread.");
        myThread.start();// 看看是否会自动执行run
    }
}

运行结果:确实自动执行Runnable接口的run了

在这里插入图片描述

Thread类

源码中有很多方法、内部类,这里先看看基础的。

构造方法

9种!!!

常用的有4种吧:

  1. 无参
	//Allocates a new Thread object. This constructor has the same effect as Thread (null, null, gname), where gname is a newly generated name. Automatically generated names are of the form "Thread-"+n, where n is an integer.
	public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }

	// 看看调用的init方法
	private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }

	// 绝了,init是个重载方法,完整版本还有2个参数
	private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) 

既然无参构造方法所调用的init方法其实共有6个参数,猜想其他构造方法也是在这6个参数的基础上变化。

  1. 带Runnable对象参数
// Runnable对象肯定实现了抽象run方法,传这个参数意味着让线程自动执行某个过程
	public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
  1. 带name参数
	// name指的是线程名称
	public Thread(String name) {
        init(null, null, name, 0);
    }
  1. 带ThreadGroup、name参数
	public Thread(ThreadGroup group, String name) {
        init(group, null, name, 0);// 线程组中的线程可访问当前线程组信息,非当前线程组的不行,感觉有点像线程池
    }

线程组还有很多fields、methods,线程组可以遍历线程,知道哪些已经运行完毕、那些还活跃。

启动start()
执行run()

实现Runnable接口的run()方法,启动(start)后自动执行

当前线程currentThread()
Thread.currentThread();
// 方法,返回当前正在被执行的线程。
设置/获取线程名set/getName()
Thread.currentThread().setName("发消息");
Thread.currentThread().getName("收消息");
// 方法,设置/获取当前正在被执行的线程的线程名。
Runnable与Thread结合使用

不建议直接用线程(Thread)对象而不结合任务(Runnable)对象,因为:

  1. JAVA只有单继承的局限性,继承了Thread就没法继承别的类
  2. 不用写Thread的子类
  3. 多个线程(对象)可以方便地执行同一个任务
(1)实现Runnable接口的类
// 用于给线程执行的任务类
class MyRunnable implements Runnable{
    @Override
    public void run(){
        // 任务内容
    }
}

// 任务类对象
MyRunnable myRunnable = new Runnable();
(2)Thread类的对象
Thread thread = new Thread(myRunnable);//跟Runnable一起用就无需写Thread类

场景2:线程中断

使用杀毒软件时,杀毒到一半,想停止杀毒。

为什么线程要中断?

正常情况,一条线程执行完run()方法后就自动结束。

实际使用中,用户应该随时可以安全地停止一个功能。

线程中断是什么?

以前JAVA提供了线程的stop()方法,但是外部stop一个线程,可能产生无法被回收的资源(文件句柄、硬件资源等),造成内存被持续占用。

因此现在用interrupt(),此时相当于抛出一个InterruptException异常,线程捕获这个异常,做出相应处理。

怎么实现安全地线程中断?

Thread thread = new Thread(runnable);
thread.start();
sleep(5);
thread.interrupt();// 中断标记

// 重写的run方法
@Override
public void run(){
  try{
    ...
  }catch (InterruptException e){
    ...// 释放资源
    return;// 线程结束
  }

场景3:守护线程

经典的守护线程,GC垃圾回收器。

守护线程是什么?

JAVA线程有两种:用户线程(user tread),守护线程(deamon thread)

用户线程:当一个进程不包含任何存活的用户线程时,进程结束。

守护线程:用于守护用户线程,当最后一个用户线程结束时,所有守护线程自动死亡。比如没了被守护者(用户线程),那GC也没有存在的意义。

不指定为守护线程的线程,都是用户线程。

怎么实现守护线程?

Thresd thread = new Thread();
thread.setDeamon(true);// 默认false,即用户线程,必须在start之前设置
thread.start();

线程状态

在这里插入图片描述

场景3:线程同步

3个窗口一起卖票,余票为0时,3个窗口都不能再买票了。

为什么线程需要同步?

主要是线程安全问题:

排队吃饭的例子、排队进试衣间、多窗口卖票的例子,不做限制的话会混乱,即不同人吃同一口饭、进同一个试衣间、卖出第-1张票。

线程同步是什么?

实际就是***一段代码***,线程需要***排队***执行这段代码,这样一来就能在这段代码开头先判断***是否***需要执行这段代码。

怎么实现线程同步?

1. 同步代码块synchronized(Object o)
//main()方法中创建3个线程来买票
Runnable ticket = new Ticket();
//都执行同一个任务
new Thread.start(ticket);
new Thread.start(ticket);
new Thread.start(ticket);

//任务类
class Ticket implements Runnable{
    //票数,3个线程共用,因为是nre Runnable的时候就调用了
    private int count = 10;
    //锁对象,3个线程共用,因为是nre Runnable的时候就调用了
    private Object o = new Object();
    
    //启动start()的时候才调用
    @Override
    public void run(){
        //不能在run里面写锁对象,因为这样就是每个线程都有自己的一把锁,根本不用排队执行,线程的锁互不干扰,自己想用就用
        
        //每个线程都循环买票,等票数小于0了,说明卖完了、应该结束买票
        while(count>0){
            //同步代码块,里面是临界区
            synchronized(o){//o改成this也行,总之需要是同一个对象
                //同步中还需要实际地判断剩余票数,达到不卖-1的票
                if(count<0){
                    System.out.println("正在准备买票...");
                    try{
                        Thread.sleep(1000);//线程休眠,单位毫秒ms
                    }catch(InterruptedException e){
                        e.pribtStackTrace();
                    }
                    count--;
                    System.out.println("卖票成功,余票:"+count);
                }else{
                    //结束线程,return也行
                    break;
                }
            }
        }
    }
}

2. 同步方法synchronized

把同步代码块的内容写成方法,这个方法用synchronized修饰,就成了同步方法。

//启动start()的时候才调用
    @Override
    public void run(){
        while(true){
            boolean flag = sale();
            if(!flag){
                break;
            }
        }
    }

//卖票过程
public synchronized boolean sale(){
    if(count<0){
    	System.out.println("正在准备买票...");
        try{
            Thread.sleep(1000);//单位:毫秒ms
        }catch(InterruptedException e){
            e.pribtStackTrace();
        }
        count--;
        System.out.println("卖票成功,余票:"+count);
        return true;
    }
    return false;         
}
3. 显示锁Lock子类ReentrantLock
//任务类
class Ticket implements Runnable{
    //票数,3个线程共用,因为是nre Runnable的时候就调用了
    private int count = 10;
    //显示锁对象
    private Lock lock = new ReentrantLock();
    
    //启动start()的时候才调用
    @Override
    public void run(){
        //每个线程都循环买票,等票数小于0了,说明卖完了、应该结束买票
        while(count>0){
            //加锁,别的线程看到有锁了,就排队
            lock.lock();
            
            //被锁住的过程,临界区
            if(count<0){
                System.out.println("正在准备买票...");
                try{
                    Thread.sleep(1000);//单位:毫秒ms
                }catch(InterruptedException e){
                    e.pribtStackTrace();
                }
                count--;
                System.out.println("卖票成功,余票:"+count);
            }else{
                //结束线程,return也行
                break;
            }
            
            //解锁
            lick.unlock()
        }
    }
}
公平锁

谁拿到锁?先来先得

//公平锁对象,传入true参数
private Lock lock = new ReentrantLock(fair: true);
非公平锁

谁拿到锁?线程一起抢。

隐式锁、显示锁默认都是***非公平锁***

隐式锁和显示锁的区别?

待补充

线程死锁

多个线程都进入了互相依赖的***同步(synchronized)***方法中。

  • 解决方法

可能产生死锁的代码中,不调用其他可能产生死锁的代码。

场景5:线程通信

生产者、消费者问题

厨师、服务员问题:

一条线程等待(wait)另一条线程唤起(notify)自己。

怎么实现线程通信?

简而言之:synchronized(同步)+wait/notify

package threadCommunication;

/**
 * 厨师先做菜,服务员再上菜
 * 厨师再做菜,服务员再上菜
 * ......
 */
public class Demo1 {
    public static void main(String[] args) {
        Food f = new Food();
        Cook cook = new Cook(f);
        Waiter waiter = new Waiter(f);
        cook.start();
        waiter.start();
    }

    // 厨师,做3道菜
    static class Cook extends Thread{
        private Food f;
        public Cook(Food f){
            this.f = f;
        }

        @Override
        public void run() {
            for (int i=0;i<3;i++){
                if (i % 2 == 0) {
                    f.setNameAndTaste("宫保鸡丁", "香辣");
                }else {
                    f.setNameAndTaste("酸菜鱼", "酸辣");
                }
            }
        }
    }

    // 服务员,端3次菜
    static class Waiter extends Thread{
        private Food f;
        public Waiter(Food f){
            this.f = f;
        }

        @Override
        public void run() {
            for (int i=0;i<3;i++){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                f.getNameAndTaste();
            }
        }
    }

    // 食物
    static class Food{
        private String name;//菜名
        private String taste;//口味
        private boolean flag=true;//做完菜的标志

        // 做菜方法,只加同步synchronized没用,这是不公平锁,厨师可能持续抢到执行的机会,不给服务员端菜的机会
        // 跟显示场景不符
        public synchronized void setNameAndTaste(String name, String taste){
            if (flag){
                this.name = name;
                // 做菜需要时间
                try {
                    System.out.println("做菜中,菜名:"+name+",口味:"+taste);
                    Thread.sleep(1000);
                    // 这里用了线程休眠,可能发生线程中断
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.taste = taste;
                flag = false;
                this.notifyAll();// 唤醒this下所有等待状态的线程
                try {
                    this.wait();//厨师等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        // 取菜方法
        public synchronized void getNameAndTaste() {
            if (!flag){
                System.out.println("服务员端走的菜名:"+this.name+",味道:"+this.taste);
                flag = true;
                this.notifyAll();
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }
    }
}

线程池

普通线程步骤:

创建线程,创建任务,执行任务,停止线程(interrupt),关闭线程(run执行完毕就自动关了)

线程池:

Executors.execute(new Runnable() {...// 常用lambda表达式
                                 });

4种默认线程池

缓存,定长,单线程,周期定长

1. 缓存线程池

长度无限制

先判断线程池中是否有空闲线程,没有的话就新建线程,可以灵活回收空闲线程

ExecutorService service = Executors.newCachedThreadPool(nThreads: n);
2. 定长线程池

参数指定线程池长度

ExecutorService service = Executors.newFixedThreadPool(nThreads: n);
3. 单线程线程池

可以按照指定顺序执行(FIFO、LIFO、优先级)

ExecutorService service = Executors.newSingleThreadExecutor();
4. 周期性定长线程池
ScheduleExecutorService service = Executors.newScheduledThreadPool(corePoolSize: n);

// 2种执行方法
// 一定时间间隔后执行一次xx任务
service.schedule(new Runnable() {}, 5, TimeUnit.SECONDS);

// 周期性执行xx任务,一直执行
service.scheduleAtFixedRate(new Runnable() {}, initDelay: n, period: m TimeUnit.SECONDS);

带返回值的接口Callable

对比Runnable接口的不同点:

  1. Callable接口使用了泛型

  2. 抽象call方法有return,也就需要指定返回值类型

FutureTask任务对象

Lambda表达式/匿名函数

函数式编程思想

只保留方法部分,跟对象无关

让接口的实现更简单,这个接口只能有一个待实现的方法

(parameters) -> expression
// 或
(parameters) ->{ statements; }
  • 实际举例
cachedThreadPool.execute(()->{
 System.out.println("线程名:" + Thread.currentThread().getName());
});
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值