栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

多线程那点事(一)

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

多线程那点事(一)

文章目录
    • 引入线程
    • 线程的执行方式
    • jconsole
    • 线程创建的四种方式
      • 继承Thread类
      • 实现Runnable接口
      • 覆写Callable接口,覆写call方法
      • 使用线程池创建线程
        • 方式1和方式2的另外3种写法
          • 使用匿名内部类继承Thread类
          • 使用匿名内部类实现Runnable接口
          • 创建线程Lambda表达式
            • 继承Thread类
            • 实现Runnable接口
    • 线程类及常见方法
      • Thread类构造方法
      • Thread的常见属性
      • 启动线程start()
      • Yield()方法
      • 中断线程的两种方式
        • 通过共享变量来进行中断
        • 调用静态方法
      • thread 收到通知的方式有两种
      • join方法
      • Thread.sleep()
      • 线程的状态
        • terminated 和 new
        • 另外3种状态
    • 线程安全问题
      • 什么是线程安全
    • 线程不安全的原因
    • JMM(java内存模型)
      • 不可见性和原子性导致的问题
      • 原子性
      • 可见性
      • 代码执行的顺序性
    • 解决线程安全?
      • synchronized关键字-监视器锁monitor lock
      • synchronized特性
        • 互斥
        • 刷新内存
        • 可重入性
          • 死锁
          • sychronized是如何支持线程的可重入?
      • synchronized使用示例
        • synchronized修饰普通方法
        • synchronized修饰静态方法
        • synchronized修饰代码块
          • 示例1
          • 示例2
          • 示例3
      • java标准库中的线程安全类

引入线程

线程并行执行

package MyThread;

import java.util.Random;


public class ThreadTestDemo {
    private static class MyThread extends Thread{
        Random random = new Random();
        @Override
        public void run() {
            while(true){
                System.out.println(Thread.currentThread().getName());
                try {
                    Thread.sleep(random.nextInt(10));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        //main方法为主进程
        //创建3个子线程
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        MyThread thread3 = new MyThread();
        
        thread1.run();
        thread2.run();
        thread3.run();

        Random random = new Random();

        //主进程
        while (true){
            System.out.println(Thread.currentThread().getName());
            try {
                Thread.sleep(random.nextInt(10));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

线程的执行方式


问题:如果上锁后,那多线程不就变成单线程了吗?

我们只是在两个线程修改同一变量的时候, 才会加锁,如果有其他线程只是读取这个变量,那是没有影响的.

如果我们顺序执行每个线程,会不会打印main方法呢?

答案:不会,要等3个线程全部执行完才会打印主方法.

这里为什么打印的一直是main?
而不是thread0,thread1,thread2,因为这块他们并不是线程,只是普通的调用run()方法而已,因为他们的主进程是main,这就是一直打印main的原因.

jconsole

在IDEA终端中使用jconsole命令可以查看当前程序线程的情况

线程创建的四种方式 继承Thread类

覆写run方法(线程的核心工作任务方法)

package innerClass;


public class ThreadMethod extends Thread{
   @Override
   public void run() {
       System.out.println("我是一个子线程");
   }

   public static void main(String[] args) {
       //创建线程类对象
       ThreadMethod thread = new ThreadMethod();
       //启动线程
       thread.start();
       //子线程输出语句
       System.out.println("子线程输出语句");
   }
}
我是实现Runnable接口,产生的线程
实现Runnable接口

覆写run方法

package innerClass;


public class ThreadMethod2 implements Runnable {
   @Override
   public void run() {
       System.out.println("我是实现Runnable接口,产生的线程");
   }

   public static void main(String[] args) {
       //创建线程任务对象
       ThreadMethod2 thread2 = new ThreadMethod2();
       //创建线程
       Thread thread = new Thread(thread2);
       //启动线程
       thread.start();
   }
}
覆写Callable接口,覆写call方法 使用线程池创建线程

前两种方式启动线程都是通过Thread类的start来启动

推荐使用线程2,

使用Runnable更加灵活,子类还能实现其他的接口,继承别的类

方式1和方式2的另外3种写法 使用匿名内部类继承Thread类
public class ThreadMethod1 {
    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                System.out.println("匿名内部类继承了Thread类");
                System.out.println(Thread.currentThread().getName());
            }
        };
        thread.start();
        System.out.println("这是主线程" + Thread.currentThread().getName());
    }
}

使用匿名内部类实现Runnable接口
    public static void main(String[] args) {
        //使用匿名内部类实现Runnable接口
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("使用匿名内部类实现Runnable接口");
                System.out.println(Thread.currentThread().getName());
            }
        });
        thread.start();
        System.out.println("这是主线程"+ Thread.currentThread().getName());
    }
}
创建线程Lambda表达式 继承Thread类
public class ThreadMethod1 extends Thread{
    public static void main(String[] args) {
        Thread thread = new Thread(() -> System.out.println("继承Thread类创建线程"));
        thread.start();
        System.out.println("这是主线程"+Thread.currentThread().getName());
    }
}
实现Runnable接口
public class ThreadMethod2 {
    public static void main(String[] args) {
        //Lambda表达式
        Thread thread = new Thread(() -> System.out.println("thread实现了Runnable接口"));
        thread.start();
        System.out.println("这是主线程" + Thread.currentThread().getName());
    }
}

线程类及常见方法 Thread类构造方法


    public static void main(String[] args) {
        Thread thread = new Thread("lisi");
        thread.start();
    }
Thread的常见属性

启动线程start()


同一个线程如果重复调用start()方法,JVM会报中断异常的错误.

Yield()方法

让出cpu,从运行状态变为就绪状态,等待操作系统的调度.

public class YieldTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (true) {
                System.out.println("张三");
                //让出CPU,从运行状态变为就绪状态,等待操作系统调度,
                // 但是到底调度部,由操作系统说了算
                Thread.yield();
            }
        }, "t1");
        t1.start();

        Thread t2 = new Thread(() -> {
            while (true) {
                System.out.println("李四");
            }
        }, "李四");
        t2.start();
    }
}
中断线程的两种方式 通过共享变量来进行中断
package MyThread;


public class ThreadInterruptByVar {
    private static class MyThread implements Runnable {
        //多个线程都要使用的变量加上volatile
        //判断当前线程是否需要停止
        volatile boolean isQuit = false;

        @Override
        public void run() {
            while (!isQuit) {
                System.out.println("别说话,正在交易~~~");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + "有内鬼,终止交易");
            System.out.println("好的,停止交易");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread t1 = new MyThread();
        Thread thread = new Thread(t1, "张三");
        System.out.println("张三,可以开始了");
        System.out.println("张三开始交易~~~");
        thread.start();
        Thread.sleep(3000);
        //如果3秒后还没完成交易就终止交易
        t1.isQuit = true;
    }
}

调用静态方法

使用Thread.interrupted()静态方法 或 Thread对象的成员方法isInterrupted()
Thread类内部包含了一个Boolean属性,来控制当前线程是否被中断.

package MyThread;


public class ThreadInterruptedByMthhod {
    public static class MyRunnable implements Runnable {

        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                //当前线程未被中断
                System.out.println(Thread.currentThread().getName() + "正在交易~~~");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println("有内鬼,终止交易~");
                    System.out.println("糟了,快跑~");
                    break;
                }
            }
        }

        public static void main(String[] args) throws InterruptedException {
            MyRunnable myRunnable = new MyRunnable();
            Thread thread = new Thread(myRunnable, "张三");
            System.out.println("时间到了,可以开始交易了");
            thread.start();
            Thread.sleep(5000);
            //调用此方法,中断线程
            thread.interrupt();
        }
    }

}

thread 收到通知的方式有两种
  1. 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通知
  • 清除中断标志.当出现 InterruptedException 的时候, 要不要结束线程取决于
    catch中代码的写法.可以选择忽略这个异常, 也可以跳出循环结束线程
  1. 只是内部的一个中断标志被设置
  • thread 可以通过Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
  • Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
    (这种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到)
package MyThread;


public class InterruptedTest {
    private static class MyRunnable implements Runnable {

        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                //此方法判断当前状态是否为true,如果为true,则中断当前状态
                System.out.println(Thread.interrupted());
                //判断当前状态是否为true,如果为true,则不清除当前状态.
//                System.out.println(Thread.currentThread().isInterrupted());
            }
        }

        public static void main(String[] args) {
            MyRunnable myRunnable = new MyRunnable();
            Thread thread = new Thread(myRunnable);
            thread.start();

            thread.interrupt();
        }
    }
}

join方法

join方法的作用:

在那个线程中调用别的线程对象的join方法,意思就是这个线程要等待另一个线程执行完毕后才会执行本线程的后续代码

主线程调用thread1.join(),主线程就会进入阻塞状态,直到thread1执行结束,才会继续执行主线程.

public class JoinTest {
    public static void main(String[] args) throws InterruptedException {
        //线程1
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "正在学习javaSE");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"张三");

        //线程2
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "正在学习数据结构");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"李四");
        t1.start();
        //等待t1执行完在执行t2
        t1.join();
        //t2开始执行
        t2.start();
    }
}
Thread.sleep()

在那调用,就休眠那个线程

线程的状态
public class StateTest {
    public static void main(String[] args) {
        for(Thread.State state : Thread.State.values()){
            System.out.println(state);
        }
    }
}

terminated 和 new

Thread-0
NEW
线程名:Thread-0  当前状态:RUNNABLE
线程名:Thread-0  当前状态:RUNNABLE
线程名:Thread-0  当前状态:RUNNABLE
线程名:Thread-0  当前状态:RUNNABLE
线程名:Thread-0  当前状态:RUNNABLE
....
....
Thread-0TERMINATED
另外3种状态

线程安全问题 什么是线程安全

即多线程环境下,并行执行和串行执行的结果是一样的,就称为线程是安全的.

//并行进行加10万操作,并行执行,每次得到的结果都不一样,
//是因为,两个线程同时对count进行修改.
//这就是线程不安全

//如果串行执行,每次得到的结果都是100000.

public class ThreadSafeTest {
    static class CounterDemo {
        public int count = 0;

        void increase() {
            count++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        CounterDemo counterDemo = new CounterDemo();
        Thread t1 = new Thread(() -> {
            for (int i = 1; i <= 50000; i++) {
                counterDemo.increase();
            }
        });
        
        Thread t2 = new Thread(() -> {
            for (int i = 1; i <= 50000; i++) {
                counterDemo.increase();
            }
        });
        t1.start();
        t2.start();
        //等待线程1执行完毕
        t1.join();
        t2.join();
        System.out.println(counterDemo.count);

    }
}
线程不安全的原因

JMM(java内存模型)

java Memory Model,这和之前所说的JVM将内存分为6大区域不相同,(JVM是实实在在将内存划分为6大区域)

java内存模型描述的是在多线程环境下,java线程内存(CPU高度缓存器和寄存器)和主内存之间的关系.

目的:屏蔽掉各种硬件和操作系统之间的速度差异,以实现在各种平台下,java程序并发执行的效果.

  • 线程之间的共享变量存在 主内存 (Main Memory).
  • 每一个线程都有自己的 “工作内存” (Working Memory) .

- 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据. 当线程要修改一个共享变量的时候

- 也会先修改工作内存中的副本, 再同步回主内存.

比如在上面的那段程序中:

不可见性和原子性导致的问题

线程1和线程2同时对count数据进行修改

假设:

  1. 线程1先开始执行,进行了5000次循环,此时count = 5000,t1将count = 5000写回了主内存
  2. t2开始执行,t2从主内存中读取到count = 5000,然后进行循环,t2一直读取的是自己的工作内存.
  3. 此时t1继续循环,此时t1线程得到的值就是50000写回到主内存
  4. 当t2,执行完循环后得到的值是55000,于是t2将55000这个值写回到主内存,最后打印得到的count = 55000

另一种情况

比如t1,和 t2 同时从内存中读取了count = 0,此时操作系统将CPU分给了t1,当t1,执行
了若干次循环之后(循环次数 < 50000) ,并将结果写回主内存,此时轮到t2线程开始执行,t2线程这个时候读取的是自己工作内存中的count = 0,t2没来得及再去读取主内存的值,当t2执行若干次循环之后,将结果值写回到主内存,就会覆盖t1写回主内存的值,这就是为什么我们看到的结果总是 < 100000的原因.


此时引入了两个问题:

  • 为啥要整这么多内存?
  • 为啥要这么麻烦的拷来拷去?
  1. 为啥整这么多内存?

实际并没有这么多 “内存”. 这只是 Java 规范中的一个术语, 是属于 “抽象” 的叫法. 所谓的 “主内存” 才是真正硬件角度的
“内存”. 而所谓的 “工作内存”, 则是指 CPU 的寄存器和高速缓存.

  1. 为啥要这么麻烦的拷来拷去?

因为 CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度(快了 3 - 4 个数量级, 也 就是几千倍, 上万倍).
比如某个代码中要连续 10 次读取某个变量的值, 如果 10 次都从内存读, 速度是很慢的. 但是如果 只是第一次从内存读,
读到的结果缓存到 CPU 的某个寄存器中, 那么后 9 次读数据就不必直接访问 内存了. 效率就大大提高了. 那么接下来问题又来了,
既然访问寄存器速度这么快, 还要内存干啥??

答案就是一个字: 贵


原子性

什么是原子性?

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入 房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性 的。 那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,
A进去就把门锁上,其他人是不是就进 不来了。这样就保证了这段代码的原子性了。 有时也把这个现象叫做同步互斥,表示操作是互相排斥的。

一条 java 语句不一定是原子的,也不一定只是一条指令

比如刚才我们看到的 n++,其实是由三步操作组成的:

  1. 从内存把数据读到 CPU
  2. 进行数据更新
  3. 把数据写回到 CPU

不保证原子性会给多线程带来什么问题

如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。

这点也和线程的抢占式调度密切相关. 如果线程不是 “抢占” 的, 就算没有原子性, 也问题不大.


可见性

什么是可见性?

可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到.

这是因为我们JMM(java内存模型)导致的,比如上面介绍的第二种情况.

代码执行的顺序性



要保证一段代码的线程安全性,就需要同时满足原子性,可见性和防止指令重排

解决线程安全? synchronized关键字-监视器锁monitor lock

解决线程安全,就是保证可见性和原子性

是否会产生线程安全问题:
要看是否多个线程在操作同一变量.

可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的 “锁定” 状态(类似于厕
所的 “有人/无人”).
如果当前是 “无人” 状态, 那么就可以使用, 使用时需要设为 “有人” 状态.
如果当前是 “有人” 状态, 那么其他人无法使用, 只能排队

注意:后面排队的线程不遵从先来后到的原则,比如厕所里面的人使用完之后,轮到后面排队的人使用,但并不是排在前面的人就可以优先使用,要等待OS的调度

public class ThreadSafeTest {
    static class CounterDemo {
        public int count = 0;

        //加锁
        synchronized void increase() {
            count++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        CounterDemo counterDemo = new CounterDemo();
        Thread t1 = new Thread(() -> {
            for (int i = 1; i <= 50000; i++) {
                counterDemo.increase();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 1; i <= 50000; i++) {
                counterDemo.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counterDemo.count);

    }
}

当有线程调用increase()时,这个线程就拿到了CounterDemo的锁,其他线程要调用increase(),必须拿到这个CounterDemo的锁,拿到则运行,否则进入阻塞状态.


//结果
100000

此时无论执行多少次,得到的结果都是100000,这说明线程是安全的.


也就是t1 和 t2 线程之间不存在 互斥现象.

关于互斥的理解: 一定要明白锁的是谁?

看一段代码:

public class ThreadSafeTest {
    static class CounterDemo {
        public int count = 0;

        //加锁
        synchronized void increase() {
            System.out.println(Thread.currentThread().getName() + "获取到锁");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        CounterDemo counterDemo1 = new CounterDemo();
        Thread t1 = new Thread(() -> {
            for (int i = 1; i <= 50000; i++) {
                counterDemo1.increase();
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 1; i <= 50000; i++) {
                counterDemo1.increase();
            }
        }, "t2");

        CounterDemo counterDemo3 = new CounterDemo();
        Thread t3 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counterDemo3.increase();
            }
        }, "t3");

        t1.start();
        t2.start();
        t3.start();
    }
}

synchronized特性

保证线程的安全就是保证线程同时具有原子性和互斥性

而sychronized就具备这些特性.

互斥

sychronized互斥性解决了原子性的问题,当一个线程执行到被sychronized修饰的对象时,此时另一个线程也执行到了这个对象,这个对象就会进入阻塞状态,等待前一个线程释放后才能执行.(相当于上锁和释放锁)

刷新内存

(天然保证互斥和可见性)

  • 原子性:只能有一个线程进入代码块,所有操作结束后才会释放锁.
  • 可见性:等到将变量写回到主内存的时候才会释放锁,其他线程在访问主内存的时候,获取的一定是修改后的变量.
可重入性

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;

死锁


这样的锁称为不可重入锁

在上面的代码中;

  1. increase 和 increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对
    this 当前 对象加锁的.

  2. 在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释 放,
    相当于连续加两次锁)

这个代码是完全没问题的. 因为 synchronized 是可重入锁.


sychronized是如何支持线程的可重入?

java的类每个都有一个对象头(描述当前对象的锁的信息,当前线程被那个对象所持有)和一个计数器(当前线程被上锁的次数)

  1. 若线程需要进入当前对象的同步代码块,如果当前对象没有上锁,计数器为1,此线程获得该对象的锁,计数器由0变为1当此对象在当前同步块中再次调用当前对象的其他同步方法时,可以进入,计数器再次+1,说明此线程在当前对象上了两次锁.

  2. 当其他线程获取此对象时,此时此对象被线程1所拥有,就会进入BLOCKED状态,等到线程1的计数器变为0时,才是真正的释放锁.

  3. 线程的计数器加减是隐式的,由JVM自动进行加减操作的.

synchronized使用示例 synchronized修饰普通方法 synchronized修饰静态方法

synchronized修饰代码块 示例1


运行结果:

示例2
public class ThreadSafeTest {
    private static class Counter {
        Object lock;

        void increase() {
            //锁的是lock这个对象,要先明确锁的是谁
            //在这段代码中,counter1和counter2中的lock对象属于同一个对象obj
            //所以它们构成互斥
            synchronized (lock) {
                while (true) {
                    System.out.println(Thread.currentThread().getName() + "获取了当前线程");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        Object obj = new Object();
        Counter counter1 = new Counter();
        counter1.lock = obj;

        Counter counter2 = new Counter();
        counter2.lock = obj;

        Counter counter3 = new Counter();
        counter3.lock = new Object();

        Thread t1 = new Thread(() -> {
            counter1.increase();
        }, "t1");

        Thread t2 = new Thread(() -> {
            counter2.increase();
        }, "t2");

        Thread t3 = new Thread(() -> {
            counter3.increase();
        }, "t3");

        t1.start();
        t2.start();
        t3.start();
    }
}
示例3
public class SynchronizedStaticTest {

    private static class Counter {
        void increase() {
            System.out.println("111");
            System.out.println("222");

            //synchronized修饰代码块,锁的粒度更细
            //this表示当前对象,锁的就是当前对象的引用
            synchronized (this) {

            }
            //任何一个类的.class对象都是全局唯一的,当JVM加载这个类的时候产生
            //描述该类的核心信息,具备哪些属性和方法,这个对象是反射类的核心
            //这个时候,这个类全局唯一,所以counter1,2,3都构成互斥关系
            synchronized (Counter.class) {
                while (true) {
                    System.out.println(Thread.currentThread().getName() + "获取到了该锁");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }

        }
    }

    public static void main(String[] args) {
        Counter counter1 = new Counter();
        Counter counter2 = new Counter();
        Counter counter3 = new Counter();

        Thread t1 = new Thread(() -> {
            counter1.increase();
        }, "t1");

        Thread t2 = new Thread(() -> {
            counter2.increase();
        }, "t2");

        Thread t3 = new Thread(() -> {
            counter3.increase();
        }, "t3");

        t1.start();
        t2.start();
        t3.start();
    }
}
java标准库中的线程安全类

Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施

多线程那点事(二)

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/999490.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号