Java学习笔记(二十三):多线程及其安全

Java学习笔记(二十三):多线程

进程

​ 要了解线程,首先得明白进程。进程就是正在运行的程序,是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。

​ 单进程计算机只能做一件事情。而我们现在的计算机都可以一边玩游戏(游戏进程),一边听音乐(音乐进程),所以我们常见的操作系统都是多进程操作系统。

​ 对于单核计算机来讲,游戏进程和音乐进程并不是同时运行的。因为CPU在某个时间点上只能做一件事情,计算机是在游戏进程和音乐进程间做着频繁切换,且切换速度很快,所以我们感觉游戏和音乐在同时进行,其实并不是同时执行的。多进程的作用不是提高执行速度,而是提高CPU的使用率。

线程

​ 在一个进程内部又可以执行多个任务,而这每一个任务我们就可以看成是一个线程。是程序使用CPU的基本单位。所以,进程是拥有资源的基本单位, 线程是CPU调度的基本单位。

​ 多线程的作用不是提高执行速度,而是为了提高应用程序的使用率。怎么理解这个问题呢?
​ 我们程序在运行的使用,都是在抢CPU的时间片(执行权),如果是多线程的程序,那么在抢到CPU的执行权的概率应该比较单线程程序抢到的概率要大。那也就是说,CPU在多线程程序中执行的时间要比单线程多,所以就提高了程序的使用率。

​ 但是即使是多线程程序,那么他们中的哪个线程能抢占到CPU的资源呢?这个是不确定的,所以多线程具有随机性。

​ 在线程中,要注意区分两个概念:并发并行

并发指应用能够交替执行不同的任务。计算机以肉眼难以跟上的速度在几个任务中不断切换,达到“同时执行”的效果,其本质上还是两个线程在分别执行。这就好比吃一口饭喝一口水, 以正常速度来看完全能够看的出来。而计算机中就是把这个过程以n倍速度执行,看起来就好像同时在吃饭和喝水一样。

​ 并行是指逻辑上同时发生。例如吃饭的时候可以边吃饭边打电话, 这两件事情可以同时执行,指的是应用能够同时执行不同的任务。

Java程序运行原理

​ Java命令会启动java虚拟机,启动JVM,等于启动了一个应用程序,也就是启动了一个进程。
​ 该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的 main 方法,所以 main方法运行在主线程中。JVM的启动是多线程的,因为其至少启动了垃圾回收线程和主线程两个线程。

多线程程序实现的方式

​ 由于线程是依赖进程而存在的,所以我们应该先创建一个进程(JVM)出来。而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。但是Java是不能直接调用系统功能的,所以我们没有办法直接实现多线程程序。因此我们需要Java去调用C/C++写好的程序来实现多线程程序,再由C/C++去调用系统功能创建进程,然后由Java去调用这样的东西,提供一些类供我们使用。这阿姨那个就可以实现多线程程序了。

实现多线程程序的方式1

  1. 创建一个继承Thread的类;
  2. 在类中重写run方法,在run方法里写入被线程执行的代码;
  3. 在主函数中创建类对象,调用start方法
public class MyThread extends Thread{
    //ctrl+O c重写父类的里面的方法
    //run()方法是让线程来调用的方法
    @Override
    public void run() {
       //子线程就是一般用来处理耗时操作的代码,
        //模拟耗时操作
        for (int i = 0; i < 100; i++) {
            System.out.println(i);
        }
    }
}

public class MyTest {
    public static void main(String[] args) {
        //1.自定义一个类,继承Thread类
        //2.创建该子类对象,重写run()方法
        //3.调用start()方法来开启线程
       // 开启一个子线程
       MyThread th = new MyThread();
       th.start();
       //th.start(); 不要重复开启同一个线程对象
        System.out.println("主线程后面的代码");
        MyThread th2 = new MyThread();
        th2.start();
    }
}

实现多线程程序的方式2

  1. 声明一个实现Runnable的接口;
  2. 重写接口中的run方法;
  3. 创建该类的对象;
  4. 创建Thread 对象时作为一个参数来传递并启动。
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            //System.out.println(this.getName()+"==="+i);
            Thread th = Thread.currentThread();
            System.out.println(th.getName() + "===" + i);
        }
    }
}

public class MyTest {
    public static void main(String[] args) {
        //这种方式扩展性强 实现一个接口 还可以再去继承其他类
       // Runnable 任务 接口应该由那些打算通过某一线程执行其实例的类来实现。
        MyRunnable myRunnable = new MyRunnable();
        Thread th1 = new Thread(myRunnable);
        th1.setName("A线程");
        th1.start();
        Thread th2 = new Thread(new MyRunable2());
        th2.setName("B线程");
        th2.start();
    }
}

实现多线程程序的方式3

​ 有一种需求:当子线程执行完,想要获取子线程执行完之后的结果,那么此时就可以使用该方法创建线程。

  1. 创建一个类实现Callable 接口 重写接口中的call方法;
  2. 创建一个FutureTask类将Callable接口的子类对象作为参数传进;
  3. 创建Thread类, 将FutureTask对象作为参数传进去;
  4. 开启线程。
public class MyCallable implements Callable<Integer> {
    private int num;
    public MyCallable(int num) {
        this.num = num;
    }
    //call()方法 让线程来执行的方法
    //call() 有返回值,而且可以抛出异常
    @Override
    public Integer call() throws Exception {
        //System.out.println("线程执行了此代码");
        //求1-num之间的和
        int sum=0;
        for (int i = 1; i <= num; i++) {
            sum+=i;
        }
        return sum;
    }
}

public class MyTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable myCallable = new MyCallable(100);
        FutureTask<Integer> task = new FutureTask<>(myCallable);
        Thread th = new Thread(task);
        th.start();
        
        //获取线程执行完之后返回的结果
        Integer integer = task.get();
        System.out.println(integer);

        MyCallable myCallable2 = new MyCallable(1000);
        FutureTask<Integer> task2 = new FutureTask<>(myCallable2);
        Thread th2 = new Thread(task2);
        th2.start();

        Integer integer1 = task2.get();
        System.out.println(integer1);

        //Runnable 任务 让线程来执行 run() 没有返回值,这个方法不能抛出异常,只能捕获异常

        //Callable 任务 让线程来执行 call() 有返回值,而且可以抛出异常。
    }
}

获取和设置线程对象名称

//public final String getName()//获取线程名称
//public final void setName(String name)//设置线程名称

public class MyThread extends Thread{
    @Override
    public void run() { //th1  th2
        for (int i = 0; i < 100; i++) {
            //this.getName() 获取线程的名字
            System.out.println("线程:"+this.getName()+":"+i);
        }
    }
}

public class MyTest {
    public static void main(String[] args) {
        //System.out.println("主线程");
       // currentThread(); 获取当前执行的线程对象
        Thread thread = Thread.currentThread();
        thread.setName("主线程");
        String name = thread.getName();
        System.out.println(name);
        MyThread th1 = new MyThread();
        MyThread th2 = new MyThread();
        MyThread th3 = new MyThread();
       //多个线程并发执行()多个抢占CPU的执行权,哪个线程抢到,在某一个时刻,就会执行哪个线程。线程的执行具有随机性。
       th1.setName("线程1");
       th2.setName("线程2");
       th3.setName("线程3");
       th1.start();
       th2.start();
       th3.start();
    }
}

对主线程进行操作的方法

//如果要获取主线程,可以通过 public static Thread currentThread()
//返回对当前正在执行的线程对象的引用
public class MyTest {
    public static void main(String[] args) {
        //System.out.println("主线程");
       // currentThread(); 获取当前执行的线程对象
        Thread thread = Thread.currentThread();
        thread.setName("主线程");
        String name = thread.getName();
        System.out.println(name);
    }
}

线程调度及获取和设置线程优先级

​ 线程有两种调度模型:

  • 分时调度模型 :所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片;

  • 抢占式调度模型 :优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。

    Java使用的是抢占式调度模型。

    注意:线程的优先级的大小仅仅表示这个线程被CPU执行的概率增大了

public final int getPriority() //获取线程的优先级


public final void setPriority(int newPriority)//设置线程的优先级

public class MyTest {
    public static void main(String[] args) {
        MyThread th1 = new  MyThread();
        MyThread th2 = new MyThread();
        MyThread th3 = new MyThread();
        //设置线程的优先级 范围1--10
        th1.setPriority(1);
        th2.setPriority(2);
        th3.setPriority(Thread.MAX_PRIORITY);
        //多个线程并发执行()多个抢占CPU的执行权,哪个线程抢到,在某一个时刻,就会执行哪个线程。线程的执行具有随机性。
        //获取线程的优先级
        int priority1 = th1.getPriority();
        int priority2 = th2.getPriority();
        int priority3= th3.getPriority();
        //线程默认的优先级是5
        //
        System.out.println("线程的优先级"+ priority1);
        System.out.println("线程的优先级" + priority2);
        System.out.println("线程的优先级" + priority3);
        //Thread.MAX_PRIORITY;
        //Thread.MIN_PRIORITY;
        //Thread.NORM_PRIORITY

        th1.setName("线程1");
        th2.setName("线程2");
        th3.setName("线程3");
        th1.start();
        th2.start();
        th3.start();

    }
}

线程控制

  1. 线程休眠sleep

    让当前正在执行的线程休眠。

    public static void sleep(long millis) 线程休眠,单位为毫秒
    
    //主线程休眠两秒后才继续执行:
    public class MyTest {
        public static void main(String[] args) throws InterruptedException {
            System.out.println("主线程开始执行了");
            Thread.sleep(2000);
            System.out.println("主线程下面的代码");
            MyThread th1 = new MyThread("A线程");
            MyThread th2 = new MyThread("B线程");
    
            th1.start();
            th2.start();
        }
    }
    
    //每个线程抢占到时间片后都会进行两秒的休眠才继续执行
    public class MyThread extends Thread{
        public MyThread(){}
        public MyThread(String name) {
            super(name);
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(1000); //单位是毫秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                String name1 = Thread.currentThread().getName();
                System.out.println(name1 + "===" + i);
            }
        }
    }
    
  2. 加入线程join

    等待该线程执行完毕了以后,其他线程才能再次执行。该方法可以让多个线程从并发执行变成串行。

    假设有三个并行的线程,其中只要有一个线程调用join的线程,且无论join位置在哪(必须在线程开始后),那么三个线程直接都变成串行执行,即:1 线程执行,执行完–> 2 线程执行,线程执行完 -->3 线程执行,执行完。

    public class MyTest {
        public static void main(String[] args) throws InterruptedException {
            MyThread th1 = new MyThread();
            MyThread th2 = new MyThread();
            MyThread th3 = new MyThread();
            th1.setName("刘备");
            th2.setName("关羽");
            th3.setName("张飞");
            th1.start();
            th1.join();
            //注意:在线程启动之后, 在调用join()方法
            th2.start();
            th3.start();
        }
    }
    
    改变join的位置,结果仍然一样:
    
    public class MyTest {
        public static void main(String[] args) throws InterruptedException {
            MyThread th1 = new MyThread();
            MyThread th2 = new MyThread();
            MyThread th3 = new MyThread();
            th1.setName("刘备");
            th2.setName("关羽");
            th3.setName("张飞");
            th1.start();
            th2.join();
            //注意:在线程启动之后, 在调用join()方法
            th2.start();
            th3.start();
        }
    }
    
    对于不同类的线程,不管是哪个类的线程调用,只要存在join,结果都会一样:
    
    public class MyTest {
        public static void main(String[] args) throws InterruptedException {
            MyThread th1 = new MyThread();
            MyThread th2 = new MyThread();
            MyThread th3 = new MyThread();
            //添加一个不同类的线程
            MyThread2 th4 = new MyThread2();
            th1.setName("刘备");
            th2.setName("关羽");
            th3.setName("张飞");
            th4.setName("黄忠");
            th1.start();
            th2.join();
            //注意:在线程启动之后, 在调用join()方法
            th2.start();
            th3.start();
            th4.start();
        }
    }
    

    如果在线程启动(start)之前调用:

    public class MyTest {
        public static void main(String[] args) throws InterruptedException {
            MyThread th1 = new MyThread();
            MyThread th2 = new MyThread();
            MyThread th3 = new MyThread();
            th1.setName("刘备");
            th2.setName("关羽");
            th3.setName("张飞");
            th1.join();
            th1.start();
            //注意:在线程启动之后, 在调用join()方法
            th2.start();
            th3.start();
        }
    }
    
    结果不会报错,但是会和平时一样随机抢占时间片。
    
  3. 守护线程setDaemon

将该线程标记为守护线程或用户线程。当主线程执行停止,守护线程也必须停止。当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。

public class MyTest {
    public static void main(String[] args) throws InterruptedException {
        Thread.currentThread().setName("刘备:主线程");
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+"=="+i);
        }

        MyThread th1 = new MyThread();
        MyThread th2 = new MyThread();
        th1.setName("张飞");
        th2.setName("关羽");
        //设置为守护线程 当主线程死亡后,守护线程要立马死亡掉。
        //注意:setDaemon(true)该方法必须在启动线程前调用。
        th1.setDaemon(true);
        th2.setDaemon(true);
        th1.start();
        th2.start();

        System.out.println(Thread.currentThread().getName()+"退出了");
    }
}
  • 用户线程和守护线程的区别:

    ​ 用户线程和守护线程都是线程,区别是Java虚拟机在所有用户线程dead后,程序就会结束。而不管是否还有守护线程还在运行,若守护线程还在运行,则会马上结束。很好理解,守护线程是用来辅助用户线程的,如公司的保安和员工,各司其职,当员工都离开后,保安自然下班了。

  • 用户线程和守护线程的适用场景
    由两者的区别及dead时间点可知,守护线程不适合用于输入输出或计算等操作,因为用户线程执行完毕,程序就dead了,适用于辅助用户线程的场景,如JVM的垃圾回收,内存管理都是守护线程,还有就是在做数据库应用的时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监听连接个数、超时时间、状态等。

  1. 中断线程

    public final void stop():		停止线程的运行
    
    public void interrupt():		清除wait(),sleep(long time)所导致的线程的阻塞状态
    

线程安全

​ 现有一个多线程售票系统:

public class CellRunnable implements Runnable{
    //这个票让三个线程共享
    static int piao=100;
    @Override
    public void run() {
        while (true) {
            if (piao >= 1) {
                    //模拟一下真实的售票环境,有网络延迟。
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在出售第:" + (piao--) + " 张票");
                }
        }
    }
}

​ 假设此时有三个售票窗口同时售票:

public class MyTest {
    public static void main(String[] args) {
        //创建了一次任务 100
        CellRunnable cellRunnable = new CellRunnable();
        Thread th1 = new Thread(cellRunnable, "窗口1");
        Thread th2 = new Thread(cellRunnable, "窗口2");
        Thread th3= new Thread(cellRunnable, "窗口3");
        th1.start();
        th2.start();
        th3.start();
    }
}

​ 执行代码,发现结果如下:

Java学习笔记(二十三):多线程及其安全_第1张图片

​ 结果中出现了重复票、0票和复数票的情况。而这种情况的出现,正是因为线程不安全所导致的。

  • 当出现负票和0票:

    由线程的随机性导致。假设某时刻只剩最后一张票了,如果此时窗口1抢到了CPU执行权,根据程序逻辑休眠50ms,而此时窗口2可以得到CPU的执行权,并且此时因为窗口1还在休眠,剩余票数仍为1张,接着陷入休眠。当窗口1苏醒时进行票数减一,剩下1张票;当窗口2苏醒,也把票数减一,此时票数为-1。

  • 当出现相同票:

    由线程的原子性导致。票的数量(假设为100)存储在主存中,此时一个线程对票进行操作时,会把票从主存中复制到自己线程的工作内存。进行减一操作后再将其返回主存。同理,其余两个线程也会将数据读取到工作内存进行操作,且三个线程返回数据的时间不确定。假设线程一先获取数据,线程二再获取数据。之后线程1先返回99,在这之后线程三读取了99,进入工作内存,返回98。而在这时,线程2执行完了并且返回,将原本的98改成了99。就因为这样,导致结果出现相同票数。

​ 导致线程不安全的因素有三个:

  1. 是否是多线程环境;

  2. 是否有共享数据;

  3. 是否有多条语句操作共享数据。

    三者存在其一就有可能导致线程不安全。而要保证线程安全,则需要使用同步代码块

    同步代码块格式:

    synchronized(对象){ //同步代码代码块上的锁,是一个互斥锁。
    	
    		死循环
    		需要同步的代码;
    	}
    

    同步可以解决安全问题的根本原因就在那个对象上,该对象如同锁的功能。

    • 同步的好处:同步的出现解决了多线程的安全问题;
    • 弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。

​ Java中锁的类型多种多样,但是基本分为三种:内置锁、对象锁和类锁。

  • 内置锁:每个java对象都可以用做一个实现同步的锁,这些锁成为内置锁。当线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁,而获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。
    java内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁。当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。

  • 对象锁:作为锁的对象为本类this。

  • 类锁:类锁是用于类的静态方法或者一个类的class对象上的,表示为 本类名.class 。

    类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
    但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。

Lock锁

​ JDK5以后提供了一个新的锁对象Lock,典型用法如下:

 class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...
   
   //lock()和unlock()之间的代码相当于同步代码块synchronized里的代码
   //lock必须被释放,因此建议和try...catch...finally方法一起使用
   public void m() { 
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }

死锁问题

​ 如果出现了同步嵌套,就容易产生死锁问题。死锁问题是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象,实例如下:

public interface ObjectUtils {
    //创建两个对象,来充当两把锁对象
    public static final Object objA=new Object();
    public static final Object objB = new Object();
}
//创建多线程
public class MyThread extends Thread {
    //标记
    boolean flag;

    public MyThread(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        if (flag) {
            synchronized (ObjectUtils.objA) {
                System.out.println("true 线程持有了objA锁,进来执行了AAAA");
                synchronized (ObjectUtils.objB) {
                    System.out.println("true 线程持有了objB锁,进来执行了BBB");
                }
            }
        } else {
            synchronized (ObjectUtils.objB) {
                System.out.println("false线程持有了objB锁,进来执行BBB");
                synchronized (ObjectUtils.objA) {
                    System.out.println("false 线程持有了objA锁,进来执行了AAA");
                }
            }
        }
    }
}
//主函数
public class MyTest {
    public static void main(String[] args) throws InterruptedException {
        MyThread th1 = new MyThread(true);
        MyThread th2 = new MyThread(false);
        th1.start();
        th2.start();
    }
}

​ 运行结果:

在这里插入图片描述

​ 原本true线程该执行的第二句”true 线程持有了objB锁,进来执行了BBB“和false线程该执行的第二句话”false 线程持有了objA锁,进来执行了AAA“都没有执行。之后双方都进入等待状态,程序无法停止。

​ 出现这种情况的原因是true线程先执行,持有了A锁。此时false线程抢占了CPU的时间片,持有了B锁。此时无论下一个时间片被哪一个线程抢到,都无法继续执行。因为执行下一句程序的锁被另外一个线程所控制,而要想得到那把锁,只有等待另外一个线程执行结束才能得到被释放的锁,因此两个线程进入死锁状态。

你可能感兴趣的:(Java,SE)