java多线程学习笔记02

x33g5p2x  于2021-11-24 转载在 Java  
字(10.6k)|赞(0)|评价(0)|浏览(291)

首先说一下自己昨天的一个疑问:

  • 线程1和2是同时启动的
  • 我在线程1的wait和线程2的notify这里点了红点然后debug
  • 为什么线程二还是会抢先在1之前全执行完
  • 要是不加红点我理解了是因为wait会瞬间释放锁而notify会执行到临界才会释放锁,但是加了红点以后按照先行发生原则不应该是虽然同时启动但是1在前面呀,1应该先输出end呢

解决:debug调试阻止不了线程执行

volatile讲解:volatile 变量规则:一个线程对某个变量的写操作,先行发生于另一个线程对这个变量的读操作

volatile 关键字:一个轻量的线程关键字,用于修饰变量,使得线程在每次使用变量的时候,都会读取变量修改后的值。

其实说白了volatile 就是一个轻量级的synchronized

volatile 和 synchronized 的比较:

  • volatile 是 synchronized 的轻量级实现;
  • volatile 只能修饰变量,synchronized 只能修饰方法和代码块;
  • volatile 不会造成阻塞,synchronized 会。

接下来我讲解一下Java中的Lock与synchronized

首先,我们要先认识到一个概念就是:可重入锁,Lock是在Java中是一个接口,我们这里在讲解Lock与synchronized的区别的时候,我们先提前讲解一下ReentrantLock与synchronized的区别,那么什么是ReentrantLock呢?其实就是Lock的一个实现类,字面意思的话就是可重入锁,那么什么是可重入锁呢?

一:可重入锁:

可重入锁是锁的一个相关概念,并不是特指我们的ReentrantLock,而是如果一个锁具备可重入性,那我们就说这是一个可重入锁。ReentrantLock和synchronized都是可重入锁。至于什么是可重入性,这里举个简单的例子,现在在一个类里我们有两个方法(代码如下)
一个叫做去北京,一个叫做买票,那我们在去北京的方法里可以直接调用买票方法,假如两个方法全都用synchronized修饰的话,在执行去北京的方法,线程获取了对象的锁,接着执行买票方法,如果synchronized不具备可重入性,那么线程已经有这个对象的锁了,现在又来申请,就会导致线程永远等待无法获取到锁。而synchronized和ReentrantLock都是可重入锁,就不会出现上述的问题。

class Trip {
    public synchronized void goToBeiJing() {
        // 去北京
        buyATicket();
    }

    public synchronized void buyATicket() {
        // 买票
    }
}

二、Lock与synchronized的不同:

二者都是可重入锁,那么为什么要有两个呢?既然存在,那么就一定是有意义的。synchronized是Java中的一个关键字,而Lock是Java1.5后在java.util.concurrent.locks包下提供的一种实现同步的方法,那么显然的,synchronized一定是有什么做不到的或者缺陷,才导致了Lock的诞生。

1.synchronized的缺点

  • (1)当一个代码块被synchronized修饰的时候,一个线程获取到了锁,并且执行代码块,那么其他的线程需要等待正在使用的线程释放掉这个锁,那么释放锁的方法只有两种,一种是代码执行完毕自动释放,一种是发生异常以后jvm会让线程去释放锁。那么如果这个正在执行的线程遇到什么问题,比如等待IO或者调用sleep方法等等被阻塞了,无法释放锁,而这时候其他线程只能一直等待,将会特别影响效率。那么有没有一种办法让其他线程不必一直傻乎乎的等在这里吗?

  • (2)当一个文件,同时被多个线程操作时,读操作和写操作会发生冲突,写操作和写操作会发生冲突,而读操作和读操作并不会冲突,但是如果我们用synchronized的话,会导致一个线程在读的时候,其他线程想要读的话只能等待,那么有什么办法能不锁读操作吗?

  • (3)在使用synchronized时,我们无法得知线程是否成功获取到锁,那么有什么办法能知道是否获取到锁吗?

2.java.util.concurrent.locks包:源码

这里总结一下:Lock一共有三大实现类:1:ReentrantLock(重入锁)2:WriteLock3:ReadLock,其中ReadLock和WriteLock是ReentrantReadWriteLock类下的两个静态内部类

Lock同时一共有五大接口方法,正如下面源码中写的那样,四个获取锁一个释放锁

public interface Lock {

// 下面这些是用来获得锁的方法:
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();

//下面这一个是用来释放锁的方法:
    Condition newCondition();
}
  • ReentrantLock是Lock的一个实现类(另外两个实现类是ReentrantReadWriteLock类下的两个静态内部类:WriteLock和ReadLock),它的意思是可重入锁,可重入锁前面已经讲过了。ReentrantLock中提供了更多的一些方法。不过常用的就是Lock中的这些。

  • 来看一下Lock接口这些方法的使用,lock()tryLock()tryLock(long time, TimeUnit unit)lockInterruptibly()是用来获取锁的。unLock()方法是用来释放锁的。

  • 这里有四个方法来获取锁,那么区别在哪里呢?

  • lock()使我们平时用的最多的,最用是用来获取锁,如果锁已经被其他线程获取,那么就等待。但是采用Lock必须要主动释放锁,所以我们一般在try{}catch{}块中处理然后在finally中释放锁,举个例子:

lock.lock();
try{
    // 处理
}catch(Exception ex){
    // 捕获异常
}finally{
    // 释放锁
    lock.unlock();
}

下面这里就是我们怎样知道获取了一个锁的方法:我们可以通过使用这两个方法从而去看返回值然后就可以知道一个线程是否获得了锁:这里就是lock比synchronized好的一个地方,synchronized是无法知道线程是否获得锁的

tryLock()是一个boolean类型的方法,当调用这个方法的时候,线程会去尝试获取锁,如果获取到的话会返回true,如果获取不到返回false,也就是说这个方法会立马返回一个结果,线程不会等待。

tryLock(long time, TimeUnit unit)是上面tryLock()方法的一个重载方法,加了两个参数,给定了等待的时间,如果在规定时间拿到锁返回true,如果拿不到返回false。这两个方法的一般用法和Lock类似。

if (lock.tryLock()) {
    try{
        // 处理
    }catch(Exception ex){
        // 捕获异常
    }finally{
        // 释放锁
        lock.unlock();
    }
}

lockInterruptibly()就比较特殊了,它表示可被中断的,意思就是,当尝试获取锁的时候,如果获取不到的话就会等待,但是,在等待的过程中它是可以响应中断的,也就是中断线程的等待过程。使用形式的话一样用try catch处理,就不贴代码了。

接下来讲解一下怎样使用ReadWriteLock:

ReadWriteLock也是一个接口,这个接口中只有两个方法,源码如下::

public interface ReadWriteLock {
2     Lock readLock();
3  
4     Lock writeLock();
5 }
  • 这个接口的从字面就能看出来他的用途,读锁和写锁,这里就是我们使用lock比使用synchronized的好处,上面synchronized缺点的第二条我写到了:synchronized会导致一个线程在读的时候,其他线程想要读的话只能等待。
  • ReentrantReadWriteLock是ReadWriteLock的一个实现类,最常用到的也是获取读锁和获取写锁。下面看例子:

首先是使用synchronized的:

public class Main {
    public static void main(String[] args)  {
        final Main m = new Main();

        new Thread(){
            public void run() {
                m.read(Thread.currentThread());
            };
        }.start();

        new Thread(){
            public void run() {
                m.read(Thread.currentThread());
            };
        }.start();
    }

    public synchronized void read(Thread thread) {
        long startTime = System.currentTimeMillis();
        while(System.currentTimeMillis() - startTime <= 1) {
            System.out.println(thread.getName()+"线程在进行读操作");
        }
        System.out.println(thread.getName()+"线程完成读操作");
    }
}
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程完成读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程完成读操作

可以看到必须线程0读完线程一才可以开始读

下面看如果用ReadWriteLock:

public class Main {
    public static void main(String[] args)  {
        private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        final Main m = new Main();

        new Thread(){
            public void run() {
                m.read(Thread.currentThread());
            };
        }.start();

        new Thread(){
            public void run() {
                m.read(Thread.currentThread());
            };
        }.start();
    }

    public void read(Thread thread) {
        readWriteLock.readLock().lock();
        try {
        long startTime = System.currentTimeMillis();
            while(System.currentTimeMillis() - startTime <= 1) {
                System.out.println(thread.getName()+"线程在进行读操作");
            }
            System.out.println(thread.getName()+"线程完成读操作");
    } finally {
        readWriteLock.unlock();
        }
    }
}
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-1线程在进行读操作
Thread-0线程在进行读操作
Thread-1线程在进行读操作
Thread-0线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-0线程在进行读操作
Thread-0线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-0线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-0线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-0线程在进行读操作
Thread-1线程在进行读操作
Thread-1线程在进行读操作
Thread-0线程在进行读操作
Thread-1线程在进行读操作
Thread-0线程在进行读操作
Thread-1线程在进行读操作
Thread-0线程在进行读操作
Thread-1线程在进行读操作
Thread-0线程在进行读操作
Thread-1线程在进行读操作
Thread-0线程在进行读操作
Thread-1线程在进行读操作
Thread-0线程在进行读操作
Thread-1线程在进行读操作
Thread-0线程在进行读操作
Thread-1线程在进行读操作
Thread-0线程完成读操作
Thread-1线程完成读操作

可以看到是交织在一起的读操作,这个结果可以看到两个线程同时进行读操作,效率大大的提升了。但是要注意的是,如果一个线程获取了读锁,那么另外的线程想要获取写锁则需要等待释放;而如果一个线程已经获取了写锁,则另外的线程想获取读锁或写锁都需要等待写锁被释放。

三、总结

总结一下二者:

  • 1.synchronized是Java的关键字,是内置特性,而Lock是一个接口,可以用它来实现同步。

  • 2.synchronized同步的时候,其中一条线程用完会自动释放锁,而Lock需要手动释放,如果不手动释放,可能会造成死锁。

  • 3.使用synchronized如果其中一个线程不释放锁,那么其他需要获取锁的线程会一直等待下去,直到使用完释放或者出现异常,而Lock可以使用可以响应中断的锁或者使用规定等待时间的锁

  • 4.synchronized无法得知是否获取到锁,而Lcok可以做到。

  • 5.用ReadWriteLock可以提高多个线程进行读操作的效率。

所以综上所述,在两种锁的选择上,当线程对于资源的竞争不激烈的时候,效率差不太多,但是当大量线程同时竞争的时候,Lock的性能会远高于synchronized。

锁:

上面我们刚刚说的synchronized 关键字和 Lock 类就是悲观锁

乐观锁就是CAS(Conmpare And Swap,比较与交换)算法和原子类,适合读操作多的场景

自旋锁 VS 适应性自旋锁

无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁:这四种锁是专门针对 synchronized 关键字的

  • 无锁:资源共享,但只有一个线程能成功操作;

  • 偏向锁:一段同步代码一直被某个线程锁获得,JDK6 之后默认启用,使用

  • XX:-UseBiasedLocking=false 关闭;

  • 轻量级锁:偏向锁被另外的线程访问时,就会升级为轻量级锁;

  • 重量级锁:轻量级锁的升级。

公平锁 VS 非公平锁:

  • 公平锁:按照申请锁的顺序,在等待队列中排队来获取锁;

  • 非公平锁:直接尝试获取锁,获取不到才会到等待队列中排队。

可重入锁 VS 非可重入锁:

  • 可重入锁:又叫递归锁,意思是同一个线程在外层方法获取到锁的时候,再进入到内层方法就会自动获取到锁(ReentrantLock 和 synchronized 都是可重入锁);

  • 非可重入锁:进入内层方法时,需要将外层锁释放,但由于线程已在方法中,无法释放,因此会造成死锁。

独享锁 VS 共享锁:

  • 独享锁:也叫排他锁,只能被一个线程所持有,既能读又能写;

  • 共享锁:可被多个线程持有,但只能读不能写;

  • 独享锁和共享锁互斥,通过 ReentrantLock 和 ReentrantReadWriteLock 实现。

分割线 ····························································································

线程池:

  • 经常创建和销毁线程,对性能的影响很大

  • 可以根据系统的需求和硬件环境灵活的控制线程的数量,且可以对所有线程进行统一的管理和控制,从而提高系统的运行效率,降低系统运行压力

分割线···················································································

线程的创建:三大方法:

首先讲第一种:Thread

2.1 步骤

  • 自定义线程类继承Thread类
  • 重写run()方法,编写线程执行体(当成main()方法用)
  • 创建线程对象,调用start()方法启动线程
  • 测试:

//主方法
public class Demo01 {
    public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        Thread2 thread2 = new Thread2();

        thread1.start();
        thread2.start();
    }
}

//100以内的偶数
class Thread1 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i%2==0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

//100以内的奇数
class Thread2 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i%2!=0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

也可以使用匿名内部类的方法来实现(线程用过以后就不再用了)

public class Demo02 {
    public static void main(String[] args) {
        //打印0~100内的偶数
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if (i%2==0){
                        System.out.println(Thread.currentThread().getName() + ":" + i);
                    }
                }
            }
        }.start();
        //打印0~100内的奇数
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if (i%2!=0){
                        System.out.println(Thread.currentThread().getName() + ":" + i);
                    }
                }
            }
        }.start();
    }
}

三个窗口同时卖票,票数总共为100张(注意票数应该是静态变量,否则就是没创建一个对象,该对象就有100张票)

package 任务十__多线程;

/** * @author ${范涛之} * @Description * @create 2021-11-23 21:30 */
public class Test {
    public static void main(String[] args) {
        Myfriend w1 = new Myfriend("范涛之 ");
        Myfriend w2 = new Myfriend("秦书翔 ");
        Myfriend w3 = new Myfriend("张容康 ");

        w1.start();
        w2.start();
        w3.start();
    }
}

class Myfriend extends Thread{
    //这里票的数量应该是静态变量,否则每个对象创建后都有100张票,而不是总共100张票
    private static int tickets = 100;

    public Myfriend(String name) {
        super(name);
    }

    @Override
    public void run() {
        while (tickets > 0){
            tickets--;
            System.out.println(getName() + "卖出了一张票,剩余票数:" + tickets);
        }
    }
}

这里可以看到我们有一个问题,那就是三个线程启动的时候票数同时变成了97!(还有一点需要注意就是票数是一个静态变量)

需要注意的小问题:

  • start()方法的作用:通过调用自己写的线程类对象的start()方法,来启动该线程,并调用该线程的run()方法
  • 不能通过直接调用run()方法的方式启动线程
  • 不可以让已经start()的线程再次star()来同时跑两个线程。可以通过新建一个该线程类的对象,然后在对新建的对象start()

Thread类中常用的方法:

  • start()启动当前线程;调用当前线程的run()方法

  • ###run():通常需要重写Thread类中的此方法,将创建线程需要执行的操作声明在此方法中(当做main()使用)
  • ###currentThread():静态方法,返回执行当前代码的线程
  • ###getName():获取当前线程的名字
  • ###setName(String name):设置当前线程的名字
  • ###yield():释放当前CPU的执行权(但也有可能下一刻的执行权又回到了当前线程,主控权还是在CPU手上)
  • ###join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完之后,线程a在结束阻塞状态
  • ###stop():当执行此方法时,强制结束当前线程(已停用)
  • ###sleep(int millitime):让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前进程是阻塞状态
  • ###isAlive():判断当前线程是否存活(线程执行完之前都是存活的)

同样是上面的三个窗口买票的问题,同样是100张票,但使用这种创建方法,tickets可以不使用静态变量:

通过实现Runnable接口来创建线程:

创建步骤

  • 创建一个实现了Runnable接口的类

  • 实现类去实现Runnable接口中的抽象方法:run()

  • 创建实现类的对象

  • 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象

  • 通过Thread类的对象调用start()

  • 这里的start()首先启动了当前的线程,然后调用了Runnable类型的target的run()

继承Thread类和实现Runnable接口两种方式比较:

开发中,优先选择实现Runnable接口的方式创建线程

原因:

  • 实现Runnable接口的方式没有类的单继承性的局限性(一个类只能继承一个父类,继承了Thread类就不能在继承其他类了)

  • 实现Runnable接口的方式更适合来处理多个线程之间有共享数据的情况

  • 联系:Thread类本身也实现了Runnable接口

线程的优先级设置:调度策略

对于同优先级的线程,组成先入先出队列(先到先服务),使用时间片策略

对于高优先级,使用优先调度的抢占式模式

线程的优先级分为1~10十个档,其中:

  • NORM_PRIORITY:5 —— 普通优先级,即默认的优先级

  • MAX_PRIORITY:10 —— 最高优先级

  • MIN_PRIORITY:1 —— 最低优先级

  • getPriority():获取线程的优先级

  • setPriority(int p):设置线程的优先级

注意:高优先级的线程要抢占低优先级线程CPU的执行权。但是只是从概率上来讲,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程被执行完以后,低优先级的线程才会被执行。

线程开启后不一定立即执行,有CPU进行调度(如果只有一个CPU,主线程和创建的线程会交替执行)

相关文章

微信公众号

最新文章

更多

目录