java多线程 (一)

104 阅读12分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情

线程介绍

进程

想要了解线程,必须先知道什么是进程, 进程是操作系统中的概念

进程: 系统中正在运行的一个应用程序。 当在系统中启动一个程序后,系统会为该程序分配至少一个进程 静态的应用程序通过运行后便产生了进程

线程(Thread)

线程也是操作系统中的概念

线程是操作系统运行调度的最小单位。 一个进程中至少会有一个线程。一个线程就是程序代码的顺序执行的过程,代码逐行执行, 执行时下面代码必须等待上面代码执行完成

在Java中主方法对应的就是程序的主线程。 当启动应用程序运行主方法就是运行在主线程中。

一个类中只能存在一个主方法, 所以只能有一个主线程

多线程

操作系统内部支持多进程,而每个进程的内部又是支持多线程的,线程是轻量级的,新建线程会共享所在进程的系统资源

目前主流的开发都是采用多线程。 多线程是采用是间片轮转法来保证多个线程的并发执行,所谓并发就是指宏观并行微观串行的机制。

多线程:一个进程中包含多个线程

多线程的调度, 执行等都是由硬件CPU负责的

利用多线程,让程序具备了多任务处理能力。

我们可以在主线程中创建多个线程,这些线程称为子线程

子线程不会阻塞主线程的执行,子线程中代码可以和主线程中代码同时执行。

并发与并行

并发

同一时刻只能有一个线程执行,但是多个线程被快速的轮换执行,使得在宏观上具有多个线程同时执行的效果,但在微观上并不是同时执行的,只是把CPU运行时间划分成若干个时间段, 再将时间段分配给各个线程执行

一个CPU(采用时间片)同时执行多个任务

image-20220331153402797.png

并行:

指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的

多个CPU同时执行多个任务

image-20220331153424565.png

5 总结:进程、线程的关系

区别进程线程
根本区别作为资源分配的单位调度和执行的单位
开 销每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销。线程可以看成时轻量级的进程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小。
所处环境在操作系统中能同时运行多个任务(程序)在同一应用程序中有多个顺序流同时执行
分配内存系统在运行的时候会为每个进程分配不同的内存区域除了CPU外,不会为线程分配内存(线程所使用的资源是它所属的进程的资源),线程内只能共享资源
包含关系没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的。线程是进程的一部分,所以线程有的时候被称为是轻权进程或者轻量级进程。

一个应用程序对应一个进程。

一个进程可以包含多个线程。

所以:一个程序关闭了,在系统中的进程就没了,程序中的线程也结束了。

创建线程

在Java中创建线程有三种实现方式。

    1. 继承Thread类,重写run方法。
  1. 实现Runnable接口,重写run方法。

    1. 实现Callable接口,结合Future

继承Thread类,重写run方法

public class Test {
    public static void main(String[] args) {
        MyThread myThread1 = new MyThread();
        myThread1.setName("兔纸");
        myThread1.start();
​
        MyThread myThread2 = new MyThread();
        myThread2.setName("王八");
        myThread2.start();
​
    }
}
 class MyThread extends Thread{
     @Override
     public void run() {
         for (int i = 1; i <= 100; i++) {
             System.out.println(Thread.currentThread().getName()+"跑了"+i+"m");
         }
     }
 }

实现Runnable接口,重写run方法

public class TestB {
    public static void main(String[] args) {
        MyRunnable myThread = new MyRunnable();
//        MyRunnable myThread2 = new MyRunnable();
        new Thread(myThread,"兔纸").start();
        new Thread(myThread,"王八").start();
​
​
​
​
    }
}
​
class MyRunnable implements Runnable {
​
    int num= 100;
    @Override
    public void run() {
        for (int i = 0; i <= num; i++) {
            System.out.println(Thread.currentThread().getName()+"跑了"+i+"m");
        }
    }
}

使用lambda表达式

public class TestB {
    public static void main(String[] args) {
​
      new Thread(()->{
           for (int i = 0; i <=100 ; i++) {
                System.out.println(Thread.currentThread().getName()+"跑了"+i+"m");
​
           }
       },"兔纸").start();
      new Thread(()->{
           for (int i = 0; i <=100 ; i++) {
               System.out.println(Thread.currentThread().getName()+"跑了"+i+"m");
​
           }
       },"王八").start();
​
    }
}
​
​

实现Callable接口,结合Future

Thread中没有提供传递Callable参数的构造方法, 实现依然使用的是参数为Runnable类型的构造方法

FutureTask实现了Runnable接口, 实现了Runnable接口中的run()方法

FutureTask也实现了Future接口, 实现了获取返回值的get()方法, 也就是说FutureTask其实就是JDK提供的Runnable接口实现类, 这个类中即有run()方法, 又有获取返回值的get()方法

Futuretask构造方法的参数需要Callable类型, Thread构造方法参数需要Runnable类型, Futuretask是Runnable的实现类, 所以Futuretask为Callable接口与Thread类之间的桥梁, 既能创建子线程, 又能获取到子线程的返回值

public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable ca = new MyCallable();
        MyCallable ca2 = new MyCallable();
​
        FutureTask<Integer> FutureTask = new FutureTask<>(ca);
        FutureTask<Integer> FutureTask2 = new FutureTask<>(ca2);
        FutureTask<Integer> FutureTask3 = new FutureTask<>(()->{
            for (int i = 0; i <= 100; i++) {
                System.out.println(Thread.currentThread().getName()+"跑了"+i+"m");
            }
            return 123;
        });
​
        new Thread(FutureTask).start();
        new Thread(FutureTask2).start();
        new Thread(FutureTask3).start();
//调用FutureTask对象的get()来获取子线程执行结束的返回值
        System.out.println(FutureTask.get());
        System.out.println(FutureTask2.get());
        System.out.println(FutureTask3.get());
​
    }
}
​
class MyCallable implements Callable<Integer>{
​
    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName());
        return new Random().nextInt(10);
    }
}
​

Callable执行流程

Thread类中调用run()方法, 传入的参数为Runnable的实现类Futuretask, 执行Futuretask中实现的run()方法, run()方法中又调用了Callable实现类中的call()方法(该方法有返回值, 使用全局变量private Object outcome;接收了call()的返回值。get()方法就是获取outcome的值

执行流程总结

  1. 执行main()方法的线程为主线程, 执行run()方法的线程为子线程
  2. main()方法是程序的入口, 对于start()方法之前的代码来说, 只有主一个线程, 当一个start()方法调用成功后线程由1个变成2个, 新启动的线程去执行run方法的代码, 主线程继续向下执行, 两个线程各自独立运行互不影响
  3. 子线程执结束和主线程执行结束, 程序结束
  4. 两个线程执行没有明确的先后执行次序, 由操作系统调度算法来决定

线程同步

线程同步:多线程在操作同一个资源时,同一时刻只能有一个线程操作,其他线程等待这个线程操作结束后抢占操作这个资源。

实现线程同步的办法就是加锁, 在java中锁有很多种,

同步代码块锁

synchronized (obj){ }

同步方法锁

private synchronized void makeWithdrawal(int amt) {}

volatile+CAS无锁化方案

Lock锁

ReentrantLock、ReentrantReadWriteLock

synchronized同步锁

在Java中每个对象或类都可以当做锁使用,这些锁称为内置锁。 Java中内置锁都是互斥锁。也就是说一个线程获取到锁,其他线程必须等待或阻塞。 如果占用锁的线程不释放锁,其他线程将一直等待下去。锁在同一时刻,只能被一个线程持有

如果锁是作用于对象,称对象锁。如果锁作用整个类称为类锁

synchronized介绍

  1. synchronized是Java中的关键字。使用synchronized关键字是锁的一种实现。

  2. synchronized的加锁和解锁过程不需要程序员手动控制,只要执行到synchronized作用范围会自动加锁(获取锁/持有锁),执行完成后会自动解锁(释放锁)

  3. synchronized可以保证可见性,因为每次执行到synchronized代码块时会清空线程区。

  4. synchronized 会不禁用指令重排,但可以保证有序性。因为同一个时刻只有一个线程能操作。

  5. synchronized 可以保证原子性, 一个线程的操作一旦开始,就不会被其他线程干扰, 只能当前线程执行完, 其他线程才可以执行。

  6. synchronized 在Java老版本中属于重量级锁(耗费系统资源比较多的锁),随着Java的不停的更新、优化,在Java8中使用起来和轻量级锁(耗费系统资源比较少的锁)已经几乎无差别了。

  7. 主要分为下面几种情况:

    1. 修饰普通方法, 非静态方法(对象锁) 需要在类实例化后, 再进行调用
    2. 修饰静态方法(类锁)静态方法属于类级别的方法, 静态方法可以类不实例化就使用
    3. 修饰代码块(对象锁、类锁)

修饰代码块

锁为固定值

当锁为固定值时,每个线程执行到synchronized代码块时都会判断这个锁是否被其他线程持有,哪个线程抢到先执行哪个线程。当抢到的线程执行完synchronized代码块后,会释放锁,其他线程竞争,抢锁,抢到的持有锁,其他没抢到的继续等待

由于值固定不变, 所有的对象调用加锁的代码块, 都会争夺锁资源, 属于类锁

public class TestD {
    public static void main(String[] args) {
        new Thread(new MyTicket(),"一号窗口").start();
        new Thread(new MyTicket(),"二号窗口").start();
    }
​
​
}
class MyTicket implements Runnable{
​
    /*
    * synchronized 修饰同步代码块
    * 锁是一个Objec类型 但需要保证多个线程中使用同一把锁才可以锁住代码
    * 1.固定值  ”lock“
    * 2.this 对象锁 操作线程传递的对象必须保证是同一个
    * Object.class  存放一个Class类型 称为类锁
    *
    * */
static int num = 1;
    @Override
    public void run() {
        while (num<=100){
            synchronized ("lock"){
                if((num<=100)){
​
                    System.out.println(Thread.currentThread().getName()+"卖出了"+num+"张票");
                    num++;
                }else {
                    System.out.println("票已经卖完");
                }
            }
            demo();
            demo2();
​
        }
    }
}

锁为this

必须是同一个对象 否则锁失效。如果是同一个对象调用synchronized所在方法时,this代表的都是一个对象。this就相当于固定值。所以可以保证结果正确性, 属于对象锁

public class TestD {
    public static void main(String[] args) {
        MyTicket t =  new MyTicket();
        //必须是同一个对象 否则锁失效
        new Thread(t,"一号窗口").start();
        new Thread(t,"二号窗口").start();
    }
​
​
}
class MyTicket implements Runnable{
​
    /*
    * synchronized 修饰同步代码块
    * 锁是一个Objec类型 但需要保证多个线程中使用同一把锁才可以锁住代码
    * 1.固定值  ”lock“
    * 2.this 对象锁 操作线程传递的对象必须保证是同一个
    * Object.class  存放一个Class类型 称为类锁
    *
    * */
static int num = 1;
    @Override
    public void run() {
        while (num<=100){
            synchronized (this){
                if((num<=100)){
​
                    System.out.println(Thread.currentThread().getName()+"卖出了"+num+"张票");
                    num++;
                }else {
                    System.out.println("票已经卖完");
                }
            }
            demo();
            demo2();
​
        }
    }
}

锁为class

锁为Class时,是一个标准的类锁, 所有的对象调用加锁的代码块都生效

public class TestD {
    public static void main(String[] args) {
        new Thread(new MyTicket(),"一号窗口").start();
        new Thread(new MyTicket(),"二号窗口").start();
    }
​
​
}
class MyTicket implements Runnable{
​
    /*
    * synchronized 修饰同步代码块
    * 锁是一个Objec类型 但需要保证多个线程中使用同一把锁才可以锁住代码
    * 1.固定值  ”lock“
    * 2.this 对象锁 操作线程传递的对象必须保证是同一个
    * Object.class  存放一个Class类型 称为类锁
    *
    * */
static int num = 1;
    @Override
    public void run() {
        while (num<=100){
            synchronized (Object.class){
                if((num<=100)){
​
                    System.out.println(Thread.currentThread().getName()+"卖出了"+num+"张票");
                    num++;
                }else {
                    System.out.println("票已经卖完");
                }
            }
            demo();
            demo2();
​
        }
    }
}

修饰实例方法

锁类型: 使用synchronized修饰实例方法时为对象锁 锁是this

锁范围: 锁的范围是加锁的方法

锁生效: 必须为同一个对象调用该方法该锁才有作用

package com.lee;
​
/**
 * @Classname TestD
 * @Description
 * @Date 2022/4/1 15:07
 * @Author Lee
 */
public class TestD {
    public static void main(String[] args) {
        MyTicket t =  new MyTicket();
        new Thread(t,"一号窗口").start();
        new Thread(t,"二号窗口").start();
    }
​
​
}
class MyTicket implements Runnable{
​
    /*
    * synchronized 修饰同步代码块
    * 锁是一个Objec类型 但需要保证多个线程中使用同一把锁才可以锁住代码
    * 1.固定值  ”lock“
    * 2.this 对象锁 操作线程传递的对象必须保证是同一个
    * Object.class  存放一个Class类型 称为类锁
    *
    * */
static int num = 1;
    @Override
    public void run() {
        while (num<=100){
​
            demo2();
​
        }
    }
​
    //synchronized 修饰成员方法 
    public synchronized void demo2()
    {
        if (num<=100){
            System.out.println(Thread.currentThread().getName()+"卖出了"+num+"张票");
            num++;
        }else{
            System.out.println("票已经卖完");
        }
    }
}
​

修饰静态方法

锁类型: 使用synchronized修饰静态方法时为类锁  锁是当前类的字节码对象

锁范围: 锁的范围是加锁的方法

锁生效: 该类所有的对象调用加锁方法, 锁都生效

package com.lee;
​
/**
 * @Classname TestD
 * @Description
 * @Date 2022/4/1 15:07
 * @Author Lee
 */
public class TestD {
    public static void main(String[] args) {
        new Thread(new MyTicket(),"一号窗口").start();
        new Thread(new MyTicket(),"二号窗口").start();
    }
​
​
}
class MyTicket implements Runnable{
​
    /*
    * synchronized 修饰同步代码块
    * 锁是一个Objec类型 但需要保证多个线程中使用同一把锁才可以锁住代码
    * 1.固定值  ”lock“
    * 2.this 对象锁 操作线程传递的对象必须保证是同一个
    * Object.class  存放一个Class类型 称为类锁
    *
    * */
static int num = 1;
    @Override
    public void run() {
        while (num<=100){
           demo();
​
        }
    }
//synchronized 修饰静态方法 类锁.class
    public  synchronized static void demo(){
        if (num<=100){
            System.out.println(Thread.currentThread().getName()+"卖出了"+num+"张票");
            num++;
        }else{
            System.out.println("票已经卖完");
        }
    }
​
​

总结:

  1. 不要将run()定义为同步方法

  2. 同步实例方法的同步监视器是this;同步静态方法的监视器是类名.class

  3. 对于synchronized锁(同步代码块和同步方法),如果正常执行完毕,会释放锁。如果线程执行异常,JVM也会让线程自动释放锁。所以不用担心锁不会释放。

  4. synchronized锁的缺点:

    如果获取锁的线程由于要等待IO或其他原因(如调用sleep方法)被阻塞了,但又没有释放锁,其他线程只能干巴巴地等待,此时会影响程序执行效率。甚至造成死锁;

    只要获取了synchronized锁,不管是读操作还是写操作,都要上锁,都会独占。如果希望多个读操作可以同时运行,但是一个写操作运行,无法实现。

死锁

死锁产生的原因:1多个线程共享多个资源 2多个线程都需要其他线程的资源,每个线程又不愿或者无法放弃自己的资源(锁的开关无法人为控制)

public class Test2 {
    public static void main(String[] args) {
        new Thread(new XiaobaiRunn()).start();
        new Thread(new XiaomingRunn()).start();
    }
}
​
class XiaomingRunn implements Runnable{
    @Override
    public void run() {
        synchronized ("遥控器"){
            System.out.println("小明抢到了遥控器,正在准备抢电池");
            synchronized ("电池"){
                System.out.println("小明抢到了电池,打开空调爽歪歪");
            }
        }
    }
}
class XiaobaiRunn extends Thread{
    @Override
    public void run() {
        synchronized ("电池"){
            System.out.println("小白抢到了电池,正在准备抢遥控器");
            synchronized ("遥控器"){
                System.out.println("小白抢到了遥控器,打开空调爽歪歪");
            }
        }
    }
}