JAVA基础:线程创建,启动,应用,线程同步,悲观锁乐观锁

1 程序 进程 线程

  • 程序: 一组静态的代码

  • 进程: 一个处于运行状态的程序 。 进程 = 执行内存 + 线程

    每当启动一个进程时,至少会有一个线程,称为:主线程

  • 线程:程序运行的过程中,真正用来执行程序功能的那个单元

    每当一个进程开启,都会产生一个主线程

    可以根据设计需求,由主线程产生更多的子线程,共同完成程序的执行过程。

    多线程的目的就是为了提高执行效率

2 创建线程

  • 创建线程类 , 暂时2种方式

    • 自定义线程类,实现Runnable接口,重写run方法 .

      实现Runnable接口的类对象,允许被多线程单独运行。

    • 自定义线程类,继承Thread父类,重写run方法

  • 创建线程对象

    • new Thread() 

class A extends Thread{
    public void run(){}
}

class B implements Runnable{
    public void run(){}
}


Thread a = new A();

B b = new B();
Thread tb = new Thread(b);

 

关于run方法

  • run方法就是线程启动后,自动调用的方法。 就如同main方法是java程序启动后,自动调用的方法

  • 我们在编码中,不能主动的调用run方法。一旦主动调用run方法,就不属于多线程操作

  • main方法是由主线程自动调用的, run方法就有子线程自动调用

  • 子线程的启动需要一定的条件。

3 启动线程

  • 线程对象在执行时,会自动调用run方法

  • 如果(手动)主动的调用run方法,那不属于线程操作,只属于简单的面向对象操作。

  • 从计算机底层来讲,真正能够执行代码指令的是CPU

  • 当线程对象执行并能够处理代码指令时,也就说明线程对象获得了CPU(操作系统)

  • Java支持的抢占式获得CPU

  • 线程抢到cpu会执行,执行一会就回释放cpu,再重新争抢。

  • 所以多线程的执行顺序是不确定。

  • 线程创建之后,必须启动才能开始争抢CPU

  • 这里我们所谓的启动线程,不是执行线程,是让线程开始争抢cpu

  • 如何启动线程呢?

  • 调用thread.start()

狭义多线程 和 广义多线程

  • 有多个线程对象,但只有1个cpu 。 称为广义多线程

  • 有多个线程对象,有多个cpu(处理器数量)

4 线程优先级

  • 线程的执行过程是需要抢占CPU的

  • 可以通过设置线程的优先级来提高线程抢占CPU的几率

  • 调用a1.setPriority(1);设置优先级

    优先级的值1 - 10

    所有新创建的线程,默认的优先级都是5

5 精灵线程

  • 也叫守护线程,守护的是主线程

  • 所有新创建的线程,称为用户线程

    如果主线程执行完毕,但用户线程没有执行完毕,主线程会等待用户线程执行完后在关闭程序

  • 守护线程是用来守护主线程的,只要主线程执行完毕,所有的守护线程立刻结束,关闭程序

  • 调用a.setDaemon(true);设置线程是否为守护线程

7 线程同步

  • 当多线程执行,对于一个共享变量进行操作时,会产生一些安全问题,称为同步问题。

  • 线程同步就是在多线程访问共享变量时,通过一定的机制,使得线程可以按照一定的顺序,每次只能有一个线程访问共享变量。

  • 针对于上述买票程序中,多个窗口卖出相同票的原因分析

    1. 每卖出一张表,都会count--

    2. 编码时这是一行代码,但jvm执行时,这可能是多行指令

      1. 从count中获取数据(放到操作数栈)

      2. 实现count-1

    3. 多个线程同时获取count参数,获取的就是同一个数值

    4. 同时会执行多次count--,从会发生,当下一个线程获取count值时不再连续了

  • 如何解决线程同步的问题呢? 可以给程序加锁

  • 加锁的特点,就是从进入加锁的代码,到执行完毕离开的这段过程中,只允许一个线程进入。

  • 加锁可以实现部分代码的串行化

  • 如何给代码片段加锁?有两种方式

    1. 悲观锁,必须要上锁,有具体的上锁操作

      悲观锁有两种,使用synchronized关键字,使用Lock对象

    2. 乐观锁,不是真正意义上的锁,没有具体的锁操作,但有一个数据状态

      每次操作数据前,会获得数据状态。在操作数据时,如果发现状态发生了变化,

      基于新状态重新操作 ​ JDK针对于数字计算,提供了原子类,底层使用的是CAS + 自旋机制

7.1 synchronized锁的使用

  • 该关键字可以给方法上锁,也可以给代码段上锁

  • 当线程调用同步方法或同步代码段时,就会获得一个对象锁

    每一个对象创建时,都会有一个对象锁,本质是一个监视器Mointer

  • 当synchronized关键字修饰的是方法时,调用该同步方法获得的是该方法所属对象的对象锁

    而不是调用这个方法的线程对象的对象锁

    当线程争抢对象锁时,如果对象所以被占用,线程将处于等待状态

    当另一个线程执行完毕,释放了占有的锁, 这些等待的线程才能继续执行。

  • 线程进入等待状态,本质就是加入了一个同步等待队列(是系统级别的)

  • 同步方法

public synchronized void t1(){
    for(int i=1;i<=10;i++){
        System.out.println(Thread.currentThread().getName()+"="+i);
    }
}

 同步代码段

public  void t2(){
        int no = (int)(Math.random()*10);
        if(no < 5){
            System.out.println("byebye");
            System.out.println("byebye");
            System.out.println("byebye");
            System.out.println("byebye");
            System.out.println("byebye");
            return ;
        }
        synchronized ("buka") {
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + "=" + i);
            }
        }

    }
    • 同步代码段所需要的锁,是通过传参指定的。只要保证传递对象是唯一的即可。

静态同步方法获得的是哪个对象的对象锁呢?

  • 获得是类模板对象的对象锁 Class对象的对象锁

  • Class类结成某一个类的类模板表示, 一个类必然只有一个类模板

7.2 Lock锁的使用

  • synchronized锁在使用时存在着一定的不足

  • 所以在jdk1.5时,提供了一个JUC java 并发 工具包。里面包含了许多与多线程操作相关的工具类

  • 其中就包含了Lock系列

    • Lock本身是一个接口,我们实际应用时使用其对应的子类ReentrantLock

  • Lock的使用过程

    1. 创建锁对象。 如果多个线程需要争抢一把锁,就创建一个锁对象。 需要多个锁,就创建多个锁对象

    2. 调用lock对象的lock()尝试争抢锁。 如果抢不到锁,线程进入等待状态。

      有一个从在方法tryLock(10,TimeUnit.SECONDS)尝试在指定的时间内获得锁。返回boolean

    3. 调用lock对象的unlock(),释放锁。 其他等待锁的线程才能继续争抢。

static final Lock lock = new ReentrantLock();
public static void t1(){
    lock.lock();
    for (int i = 1; i <= 10; i++) {
        System.out.println(Thread.currentThread().getName() + " : " + i);
    }
    lock.unlock();
}

 

在使用lock锁,建议将获得锁的代码写在try中,将释放锁的代码写在finally中

确保无论操作是否成功还是是否,都能释放锁。

try{
    lock.lock();
    ...
}finally{
    lock.unlock();
}

lock锁底层机制

  • 底层使用的是CAS + AQS

    1. 在lock底层有一个计数器,记录锁被获取的状态,起初为0 , 当被抢占的时候变为1

      也就是说,当我们调用lock.lock()方法,就是将状态从0改为1的过程

      当我们调用lock,unlock()方法时,就是将状态从1改为0的过程

      当我们调用lock.lock方法时,如果发现状态是1,表示锁被占用,当前线程进入等待状态

    2. 当多个线程同时访问lock.lock()方法时,就想将状态从0改为1

      每个线程都尝试着将状态从0改为1. 假设a线程最先完成状态改变,a线程获得了锁

      此时b线程也尝试将0改为1,先获得原始值0,将0改为1,再将改好的1替换原始值0(赋值)

      在替换前,会拿着之前的原始值与现在变量里的值进行比较,看看是否发生了变化

      如果没有变化,说明这个过程中没有其他线程访问资源,将其改为1获得锁。

      如结发生了变化,说明这个过程中被其他线程捷足先登了,其他的线程获得锁,当前线程等待

      我们称这个过程为 CAS (compare and set)

    3. 当一个线程获得锁时,发现锁已经被占用了,当前这个线程就会处于等待状态

      实际上,并不是线程对象有一个状态码,改为等待状态的值。

      实际上是将需要等待的线程,存入了一个集合,并使其进入最终等待状态(jvm级别的等待状态)

      当最开始线程执行完毕释放锁后,就会从这个集合中取出最开始的那个线程继续执行

      这个集合我们就称为 AQS (抽象的)队列同步器

7.3 乐观锁

  • 乐观锁不是一种真正存在的锁,一种机制

  • 底层使用的是 CAS + 自旋 应用组合

    //count++;
    //一假设线程1获得count值为0并记录为原始值
    //二接着会基于0实现++操作,变成1
    //三接着会尝试将1赋值回count变量中
    //  1. 根据之前记录的原始值0 与此时此刻count变量中的值比较
    //  2. 如果相等,说明这个过程中没有其他线程修改count值,表示这段时间当前线程占有这变量
    //     完成赋值
    //  3. 如果不相等。比如count已经=2了,说明这个过程中,有其他线程使用过这个变量
    //     如果继续赋值,影响了其他线程的操作结果,所以不能赋值,此次操作失败
    //     重新获取现在的最新的值,记录为原始值2,重复一操作 ,直到能够完成赋值位置
    //     我们称这个重复过程为:自旋
  • 乐观锁的使用更具有局限性, 适用于数字变量的计算,一般多是 ++ 和 --

  • JDK中提供了一个原子类 AtomicIntger,该类对象中提供了++和--的计算方法

    当通过该类对象提供的++和--的方法计算时,可以确保线程同步。

AtomicInteger的常用方法

static AtomicInteger count = new AtomicInteger(0);

count.get(); //获得变量值(不是同步的)
count.set(value);//给变量赋值(不是同步的)

count.getAndIncrement(); //等价于count++
count.incrementAndGet(); //等价于++count
count.getAndDecrement(); //等价于count--
count.decrementAndGet(); //等价于--count
count.getAndAdd(v); //等价于count += v

注意1:CAS的过程会不会出现同步问题

  • 不会出现同步问题,CAS底层是基于系统实现的

  • 实现时最终也会上锁。 也就是当一个线程在CAS时,其他线程处于等待状态

  • 虽然最终都上锁了,但与被锁的颗粒度不同。

注意2:如何解决CAS过程中的ABA问题

  • CAS特点是,在set设置新值之前,先用原始值与当前变量中的值做比较

  • 相等说明这个过程中没有其他线程操作变量,也就相当于当前线程占有变量

  • 不相等说明这个过程中有其他线程操作变量,也就是这个变量其他线程占有,我需要重新获得

  • 现在的问题是,在比较时原始值与当前变量中的值相等,就能说明这个过程中变量没有被其他线程操作么? 不能

  • 因为有可能另一个线程将变量中的值,从A改成了B又改回成了A

  • ABA问题如何解决呢? 可以为数据增加一个版本号,只要改变过,版本号就+1

  • JDK提供了一个可以解决ABA问题的原子类

public static void main(String[] args) throws InterruptedException {
    AtomicStampedReference<Integer> count = new AtomicStampedReference<>(0,1);

    Thread t1 = new Thread(()->{
        Integer oldValue = count.getReference();
        int oldVerion = count.getStamp();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        boolean f = count.compareAndSet(oldValue,oldValue+1,oldVerion,oldVerion+1);
        System.out.println(Thread.currentThread().getName()+" : " + f);
    });
    t1.start();

    Thread.sleep(100);

    Thread t2 = new Thread(()->{
        Integer oldValue = count.getReference();
        int oldVerion = count.getStamp();

        boolean f = count.compareAndSet(oldValue,oldValue+1,oldVerion,oldVerion+1);
        System.out.println(Thread.currentThread().getName()+" : " + f);

        oldValue = count.getReference();
        oldVerion = count.getStamp();

        f = count.compareAndSet(oldValue,oldValue-1,oldVerion,oldVerion+1);
        System.out.println(Thread.currentThread().getName()+" : " + f);
    });
    t2.start();
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值