【Java系列】多线程篇

目录

二、如何在代码中创建线程

1.继承Thread类

2.实现Runnable接口

3.Thread和Runnable的区别

三、启动线程

1.如何理解t.start()做了什么?

2.为什么大概率先执行主线程?

四、常用函数说明

1.sleep(long millis)

2.join()

3.yield()

4.setPriority()和getPriority()

5.interrupt()

6.currentThread()

五、线程安全(重点)

1.什么是线程不安全

2.线程不安全的原因

站在开发者的角度

站在系统的角度   

六、锁机制

1.synchronize锁

 2.JUC包(重点)

七、volatile机制

八、设计模式 (重点)

1.饿汉模式

2.懒汉模式

九、wait()和notify() 

1.wait()


一、Java线程在代码中如何体现?

        java.lang.Thread类(包括子类)的一个对象

二、如何在代码中创建线程

1.继承Thread类

继承Thread类,并重写run方法

public class MyFirstThreadClass extends Thread{
    @Override
    public void run() {
        //这个方法下写的所有代码,如果正确创建线程的话,都会运行在新的线程执行流中
        System.out.println("这是我的第一个线程");
    }
}

实例化该类对象

public class Main {
    public static void main(String[] args) {
        MyFirstThreadClass t = new MyFirstThreadClass();
        // t 指向一个创建出来的线程对象
    }
}

2.实现Runnable接口

实现Runnable接口,并重写run方法

public class MyFirstTask implements Runnable{
    @Override
    public void run() {
        System.out.println("这是我的第一个任务的第一句话");
    }
}

实例化Runnable对象,利用该对象去构建Thread对象 

public class Main {
    public static void main(String[] args) {
        MyFirstTask task = new MyFirstTask();   //创建了一个任务对象
        Thread t = new Thread(task);            //把 task 作为 Thread 的构造方法传入,此时语句就运行在行的线程中了
    }
}

3.Thread和Runnable的区别

        如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。

        实现Runnable接口比继承Thread类所具有的优势:

        1):适合多个相同的程序代码的线程去处理同一个资源

        2):可以避免java中的单继承的限制

        3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立

        4):线程池只能放入实现Runable线程,不能直接放入继承Thread的类

三、启动线程

        当手中有一个Thread对象时,调用其start()方法。

        注意:①一个已经调用过start()后不可以再调用;②千万不要调用成run();

public class Main {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
    }
}

1.如何理解t.start()做了什么?

        把线程的状态从新建变成了就绪。线程被加入线程调度器的就绪队列中,等待被调度器选中分配CPU。

        从子线程进入就绪态开始,子线程和主线程在低位上就完全平等了,此时哪个线程被选中分配CPU完全是听天由命。

2.为什么大概率先执行主线程?

        t.start()是主线程语句。换言之,这条语句被执行了,说明主线程现在还在CPU上。而刚执行完t.start()就立马发生线程调度的概率不大,所以大概率先执行主线程。

四、常用函数说明

1.sleep(long millis)

         在指定的毫秒数内让当前正在执行的线程休眠。从线程的状态角度来看,就是让当前线程从“运行”→“阻塞”。

2.join()

        join是Thread类的一个方法,其作用是:“等待该线程终止”。这里需要理解的就是该线程是指主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。

        为什么要用join()方法?
        在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。

public class Join {
    private static class B extends Thread{
        //模拟B要做一个长时间任务
        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            println("B拿到了钱");

            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            println("B把钱送给A");
        }

        private static void println(String msg){
            Date date = new Date();
            DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            System.out.println(format.format(date) + ":" + msg);
        }

        public static void main(String[] args) throws InterruptedException {
            B b = new B();
            b.start();
            println("A吃饭但没钱");
            //观察有join和没有join的区别
            b.join();
            println("A结账走人");

        }
    }
}

输出结果如下:

2022-07-22 18:17:00:A吃饭但没钱
2022-07-22 18:17:05:B拿到了钱
2022-07-22 18:17:10:B把钱送给A
2022-07-22 18:17:10:A结账走人

3.yield()

        暂停当前正在执行的线程对象,并执行其他线程。从线程的状态角度来看,就是让当前线程从“运行”→“就绪”。随时可以继续被调度回CPU。

        yield()主要用于执行一些耗时比较久的计算机任务,为了避免“卡顿”,时不时让出一些CPU资源给OS中的其他进程。

sleep()和yield()的区别:
        sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到就绪态,所以执行yield()的线程有可能在进入到就绪态后马上又被执行。
        sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。

        实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU  的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程
       另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield()  方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。

4.setPriority()和getPriority()

 更改/查询线程的优先级

Thread4 t1 = new Thread4("t1");
Thread4 t2 = new Thread4("t2");
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);
public class Priority {
    public static void main(String[] args) {
        Thread main = Thread.currentThread();
        System.out.println(main.getPriority());

        main.setPriority(Thread.MAX_PRIORITY);
        System.out.println(main.getPriority());

        main.setPriority(Thread.NORM_PRIORITY);
        System.out.println(main.getPriority());

        main.setPriority(Thread.MIN_PRIORITY);
        System.out.println(main.getPriority());
    }
}

setPriority()也只是给JVM建议,不可以强制让哪个线程优先调度。

5.interrupt()

        不要以为它是中断某个线程!它只是向线程发送一个中断信号,告诉他要停止,实际上并不会影响线程运行。

        线程处于休眠状态时(比如:sleep,join)意味着线程无法立马执行interrut()。此时,JVM的处理方式是以异常形式通知线程。当线程捕获到异常就知道有人让他停止了。

public class Interrupt {
    static class B extends Thread{
        @Override
        public void run() {
            while (true){
                if(Thread.interrupted()){
                    //休息后有人要我停止
                    break;
                }

                for (int i = 0; i < 1000; i++) {
                    System.out.println("我正在工作哟");
                }

                if(Thread.interrupted()){
                    //休息前有人要我停止
                    break;
                }

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    //休息的时候有人要我停止
                    break;
                    //大概率是在这里停下,因为工作只用毫秒即可完成,但是休息可以休息一秒
                }
            }
            System.out.println("我不工作咯");
        }
    }

    public static void main(String[] args) {
        B b = new B();
        b.start();

        Scanner s = new Scanner(System.in);
        s.nextLine();   //主线程在此阻塞,直到有用户输入

        b.interrupt();
    }
}

6.currentThread()

        返回一个Thread引用,指向一个线程对象;在那个线程中调用该方法就返回哪个对象。

五、线程安全(重点)

1.什么是线程不安全

        单看代码没有问题,但是结果不符合预期就称为线程不安全。

2.线程不安全的原因

  • 站在开发者的角度

    多个线程之间操作了同一块数据,且至少有一个线程正在修改这块共享数据。   
  • 站在系统的角度   

        ①原子性被破坏(最常见的原因)

        程序员的预期中r++和r--是一个原子性操作,但实际执行起来保证不了原子性;COUNT越大,线程执行需要跨时间片的概率越大(碰到线程调度的概率越大)导致出错率越大。

public class NoSafe {
    // 定义一个共享的数据 —— 静态属性的方式来体现
    static int r = 0;

    // 定义加减的次数
    // COUNT 越大,出错的概率越大;COUNT 越小,出错的概率越小
    static final int COUNT = 100000;

    // 定义两个线程,分别对 r 进行 加法 + 减法操作
    static class Add extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < COUNT; i++) {
                r++;
            }
        }
    }

    static class Sub extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < COUNT; i++) {
                r--;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Add add = new Add();
        add.start();

        Sub sub = new Sub();
        sub.start();

        add.join();
        sub.join();

        // 理论上,r 被加了 COUNT 次,也被减了 COUNT 次
        // 所以,结果应该是 0
        System.out.println(r);
    }
}

常见的违反原子性的场景:

①read-write场景 

  1. i++;
  2. arr[size] = e;size++;

②check-update场景

        if(a == 10){

                a = 20;

        }

     ②内存可见性

        线程所有的数据操作(读/写)必须:①从主存加载到工作内存;②在工作内存中进行处理;③处理完毕后,再将数据同步回主存。

        这就导致了内存可见性问题:一个线程对数据的操作,很可能导致其他线程无法感知。

理解:班级中有个账本(主存)班长出去买东西,班费减少,但未记录在账本上,只有班长脑子里(工作内存)知道这件事儿,同时学委出去采购,就会导致超支。    

     ③重排序

        我们写的程序往往经过了中间很多环节优化的结果,并不保证最终执行的和我们写的一样。作为应用开发,是无法得知做了什么优化的。所谓重排序就是指执行的指令和书写的指令不一样。

六、锁机制

1.synchronize锁

  • 修饰方法(普通,静态)

  • 同步代码块

public class UseSync {
    // sync 修饰普通方法
    public synchronized int add() {
        return 0;
    }

    //等价于
    public int add_2(){
        synchronized (this){
            return 0;
        }
    }

    // sync 修饰静态方法
    private  synchronized static int sub() {
        return 0;
    }

    //等价于
    private static int sub_2(){
        synchronized (UseSync.class){
            return 0;
        }
    }

    // sync 同步代码块
    public void method() {
        Object o = new Object();
        synchronized (o) {

        }
    }

    // sync 同步代码块
    public void method2() {
        synchronized (UseSync.class) {

        }
    }
}

        锁理论上就是一段被多个线程共享的数据

sync(ref){        //对ref引用指向的对象加锁

        //执行一些语句

}        //解锁

多个线程都有加锁操作时,且申请的是同一把锁,就会造成加锁某代码解锁。临界区代码就会互斥进行。例:

public class Demo {
    // 这个对象用来当锁对象
    static Object lock = new Object();

    static class MyThread1 extends Thread {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 1000; i++) {
                    System.out.println("我是张三");
                }
            }

            for (int i = 0; i < 1000; i++) {
                System.out.println("我是王五");
            }
        }
    }

    static class MyThread2 extends Thread {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 1000; i++) {
                    System.out.println("我是李四");
                }
            }

            for (int i = 0; i < 1000; i++) {
                System.out.println("我是赵六");
            }
        }
    }

    public static void main(String[] args) {
        Thread t1 = new MyThread1();
        t1.start();


        Thread t2 = new MyThread2();
        t2.start();
    }
}

 2.JUC包(重点)

         java.util.concurrent.*;是现代java并发编程常用的包。

  • java.util.concurrent.locks;下的Lock接口
  1. void lock();        等同于sync
  2. void lockInterruptibly90;        加锁,但是允许被打断
  3. boolean tryLock();        加锁失败返回fals
  4. boolean tryLock(long time, TimeUnit unit);        时间段内加锁,失败返回fals
  5. void unlock();        解锁

使用方法:

Lock l = new ReentrantLock();

l.lock();

try{

        //临界区代码

}finally{

        l.unlock();        //保证解锁

}

public class NoSafeWithLock {
    // 定义一个共享的数据 —— 静态属性的方式来体现
    static int r = 0;
    // 定义加减的次数
    // COUNT 越大,出错的概率越大;COUNT 越小,出错的概率越小
    static final int COUNT = 100_0000;

    // 定义两个线程,分别对 r 进行 加法 + 减法操作
    // r++ 和 r-- 互斥
    static class Add extends Thread {
        private Lock o;
        Add(Lock o) {
            this.o = o;
        }

        @Override
        public void run() {
            o.lock();
            try {
                for (int i = 0; i < COUNT; i++) {
                    r++;    // r++ 是原子的
                }
            } finally {
                o.unlock();
            }
        }
    }

    static class Sub extends Thread {
        private Lock o;

        Sub(Lock o) {
            this.o = o;
        }

        @Override
        public void run() {
            o.lock();
            try {
                for (int i = 0; i < COUNT; i++) {
                    r--;    // r-- 是原子的
                }
            } finally {
                o.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Lock lock = new ReentrantLock();

        Add add = new Add(lock);
        add.start();

        Sub sub = new Sub(lock);
        sub.start();

        add.join();
        sub.join();

        // 理论上,r 被加了 COUNT 次,也被减了 COUNT 次
        // 所以,结果应该是 0
        System.out.println(r);
    }
}

七、volatile机制

        用于修饰变量,线程要读写变量,每次从主存读,写入时要保证写会主存,用于保护内存可见性。

public class UseVolatile {
    volatile static boolean quit = false;
    //static boolean quit = false;

    static class MyThread extends Thread{
        @Override
        public void run() {
            long r = 0;
            while (quit == false){
                r++;
            }
            System.out.println(r);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.start();
        TimeUnit.SECONDS.sleep(5);
        quit = true;
    }
}

八、设计模式 (重点)

        对一些解决通用问题而经常书写的代码片段的总结与归纳

1.饿汉模式

        一开始就初始化

public class StarvingMode {
    int a = 10;
    // 是线程安全的

    // static类加载的时候执行
    // JVM 保证了类加载的过程是线程安全的
    private static StarvingMode instance = new StarvingMode();

    private static StarvingMode getInstance() {
        return instance;
    }

    //privat构造方法保证无法初始化其他对象
    private StarvingMode() {}

}

2.懒汉模式

        用到了再初始化

public class LazyMode {
    private volatile static LazyMode instance = null;

    public static LazyMode getInstance() {
        // 第一次调用这个方法时,说明我们应该实例化对象了
        if (instance == null) {
            // 只有 instance 还没有初始化时,才会走到这个分支
            // 这里没有锁保护,所以理论上可以有很多线程同时走到这个分支

            synchronized (LazyMode.class) {   
                // 通过上面的条件,
                // 让争抢锁的动作只在 instance 实例化之前才可能发生,
                // 实例化之后就不再可能发生。

                // 加锁之后才能执行
                // 第一个抢到锁的线程,看到的 instance 是 null
                // 其他抢到锁的线程,看到的 instance 不是 null
                // 保证了 instance 只会被实例化一次
                if (instance == null) {
                    instance = new LazyMode();    // 只在第一次的时候执行
                }
            }
        }
        return instance;
    }
    private LazyMode() {}
}

九、wait()和notify() 

        wait() 和 notify() 方法属于Object类,Java中的对象都自带这俩方法。要使用他们必须先对对象进行sync加锁。

1.wait()

        使当前线程等待,直到另一个线程为此对象调用notify()方法或notifyAll()方法

public class Demo1 {
    static class MyThread extends Thread {
        private Object o;

        MyThread(Object o) {
            this.o = o;
        }

        @Override
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            synchronized (o) {
                System.out.println("唤醒主线程");
                o.notify();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();

        synchronized (o) {
            //放在锁里面,保证是主线程先执行
            MyThread t = new MyThread(o);
            t.start();

            o.wait();
            //1.释放o锁
            //2.等待
            //3.再次加锁
            System.out.println("唤醒后到达");
        }
    }
}

 2.notify()

  • 随机唤醒
public class Demo3 {
    static Object o = new Object();

    static class MyThread extends Thread {
        @Override
        public void run() {
            synchronized (o) {
                try {
                    o.wait();
                    System.out.println(getName());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            MyThread t = new MyThread();
            t.start();
        }

        // 保证了子线程们先 wait,主线程就先休眠一会儿
        TimeUnit.SECONDS.sleep(5);

        synchronized (o) {
            o.notify();
 //          o.notifyAll();
        }
    }
}
  •  wait-notify是没有状态保存的,换言之,先notify后wait就会导致wait感知不到之前的notify,就会永远等待下去。
public class Demo4 {
    static Object o = new Object();

    static class MyThread extends Thread {
        @Override
        public void run() {
            synchronized (o) {
                System.out.println("notify");
                o.notify();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.start();

        TimeUnit.SECONDS.sleep(2);

        synchronized (o) {
            System.out.println("wait");
            o.wait();
        }
        System.out.println("解锁成功");
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值