JUC并发编程(一)之线程精讲篇

1.线程、进程认识

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位

进程的执行逻辑
通过CPU切换时间切片,执行相应进程
在这里插入图片描述

2.为什么要有线程

1.效率方面分析
同一进程中,如果需要执行多个任务,如果其中一个任务被阻塞,其他不依赖该任务的任务也会被阻塞

2.系统开销上分析
线程可以认为是轻量级的进程,线程的创建和销毁比进程快

3.创建线程方式

继承Thread类

public class TestThread extends Thread{
    
    

    int i=3;
    
    public static void main(String[] args) {
    
    
        TestThread testThread=new TestThread();
        TestThread testThread2=new TestThread();
        testThread.start();
        testThread2.start();
    }

    @Override
    public void run() {
    
    
        for(;i<5;i++) {
    
    
            System.out.println("test"+i);
        }
    }
}

在这里插入图片描述
注意:
这里的 i 是实例变量而不是局部变量。因为:通过继承Thread类实现多线程时,每个线程的创建都要创建不同的子类对象,导致Thread-0 、Thread-1两个线程不能共享成员变量 i

实现Runnable接口

这种方式创建并启动多线程的步骤如下:
1、定义一个类实现Runnable接口;
2、创建该类的实例对象obj;
3、将obj作为构造器参数传入Thread类实例对象,这个对象才是真正的线程对象;
4、调用线程对象的start()方法启动该线程;

public class TestRunable implements Runnable{
    
    

    int i=3;

    public static void main(String[] args) {
    
    
        TestRunable testRunable=new TestRunable();
        Thread thread=new Thread(testRunable);
        Thread thread2=new Thread(testRunable);
        thread.start();
        thread2.start();
    }


    @Override
    public void run() {
    
    
        for(;i<5;i++) {
    
    
            System.out.println("test"+i);
        }
    }
}

运行第一次结果
在这里插入图片描述
运行第二次结果
在这里插入图片描述
结果分析:

1、实现Runnable接口的类的实例对象仅仅作为Thread构造器的参数,Runnable实现类里包含的run()方法仅仅作为线程执行体,而实际的线程对象依然是Thread实例,这里的Thread实例负责执行其的run()方法;

2、线程1和线程2输出的成员变量i是连续的,也就是说通过这种方式创建线程,可以使多线程共享线程类的实例变量,因为这里的多个线程都使用了同一个实例变量。

3.会造成线程并发问题(运行结果二),解决需要加锁(Synchronized)或者用Volatile关键字将变量设置为共享变量,Volatile关键字下面会介绍

Callable接口

1.使用Callable和Future的完整示例

public class TestCallable implements Callable<String> {
    
    


    public static void main(String[] args) {
    
    
        System.out.println("----程序开始运行----");
        Date date1 = new Date();
        int taskSize=5;
        ExecutorService pool = Executors.newFixedThreadPool(taskSize);
        List<Future> list = new ArrayList<Future>();
        for (int i = 0; i < taskSize; i++) {
    
    
            Callable c = new TestCallable();
            // 执行任务并获取Future对象
            Future f = pool.submit(c);
            list.add(f);
        }
        // 关闭线程池
        pool.shutdown();
        // 获取所有并发任务的运行结果
        for (Future f : list) {
    
    
            // 从Future对象上获取任务的返回值,并输出到控制台
            try {
    
    
                System.out.println(">>>" + f.get().toString()); //OPTION + return 抛异常
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            } catch (ExecutionException e) {
    
    
                e.printStackTrace();
            }
        }
        Date date2 = new Date();
        System.out.println("----程序结束运行----,程序运行时间【" + (date2.getTime() - date1.getTime()) + "毫秒】");
    }

    @Override
    public String call() throws Exception {
    
    


        return "callable call接口测试";
    }
}

2.使用Callable和FutureTask的完整示例

public class TestCallable2 implements Callable<String> {
    
    

    public static void main(String[] args) {
    
    
        System.out.println("----程序开始运行----");
        Date date1 = new Date();


        FutureTask[] futureTasks = new FutureTask[5];


        for (int i = 0; i < 5; i++)
        {
    
    
            Callable callable = new TestCallable2();


            // Create the FutureTask with Callable
            futureTasks[i] = new FutureTask(callable);


            // As it implements Runnable, create Thread
            // with FutureTask
            Thread t = new Thread(futureTasks[i]);
            t.start();
        }
        // 关闭线程池

        // 获取所有并发任务的运行结果
        for (int i = 0; i < 5; i++)
        {
    
    
            try {
    
    
                System.out.println(futureTasks[i].get());
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            } catch (ExecutionException e) {
    
    
                e.printStackTrace();
            }
        }


        Date date2 = new Date();
        System.out.println("----程序结束运行----,程序运行时间【" + (date2.getTime() - date1.getTime()) + "毫秒】");
    }


    @Override
    public String call() throws Exception {
    
    

        return "callable call接口测试";
    }
}

在这里插入图片描述

4.线程生命周期

线程的生命周期包含5个阶段,包括:新建、就绪、运行、阻塞、销毁。

  • 新建:就是刚使用new方法,new出来的线程;
  • 就绪:就是调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行;
  • 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能;
  • 阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态;
  • 销毁:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源;

针对各种状态进行详细分析
新建状态

Thread t1 = new Thread();

这里的创建,仅仅是在JAVA的这种编程语言层面被创建,而在操作系统层面,真正的线程还没有被创建。

就绪状态
调用start()方法后,JVM 进程会去创建一个新的线程,而此线程不会马上被 CPU 调度运行,进入Running状态,这里会有一个中间状态,就是Runnable状态,你可以理解为等待被 CPU 调度的状态

t1.start()

运行状态
当CPU调度发生,并从任务队列中选中了某个Runnable线程时,该线程会进入Running执行状态,并且开始调用run()方法中逻辑代码。

阻塞状态
被转换成Blocked状态,比如调用了sleep, wait
被转换成Blocked状态,比如获取某个锁的释放,而被加入该锁的阻塞队列中;

终止状态
一旦线程进入了Terminated状态,就意味着这个线程生命的终结,哪些情况下,线程会进入到Terminated状态呢?
线程正常运行结束,生命周期结束;
线程运行过程中出现意外错误;

阻塞状态怎么转换成runable状态
完成了指定时间的休眠,进入到Runnable状态;
正在wait中的线程,被其他线程调用notify/notifyAll方法唤醒,进入到Runnable状态;
线程获取到了想要的锁资源,进入Runnable状态;

Running状态转Runable状态
该线程的时间片用完,CPU 再次调度,进入Runnable状态;

Runable状态转Running状态
线程主动调用 yield 方法,让出 CPU 资源,进入Runnable状态

5.线程操作及原理分析

join方法
1.Thread中,join()方法的作用是调用线程等待该线程完成后,才能继续向下运行。

2.t.join()方法只会使主线程进入等待池并等待t线程执行完毕后才会被唤醒。并不影响同一时刻处在运行状态的其他线程。
比如:
t1.start();
t2.start();
t1.join();
t3.start();

t1,t2会继续执行,t3会等到t1执行完之后执行

3.原理分析

public final synchronized void join(long millis)
throws InterruptedException {
    
    
    long base = System.currentTimeMillis();
    long now = 0;


    if (millis < 0) {
    
    
        throw new IllegalArgumentException("timeout value is negative");
    }


    if (millis == 0) {
    
    
        //会使主线程处于等待状态
        while (isAlive()) {
    
    
            wait(0);
        }
    } else {
    
    
        while (isAlive()) {
    
    
            long delay = millis - now;
            if (delay <= 0) {
    
    
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

跟据源码我们看到:当某个线程调用join方法后,会使主线程wait进行阻塞状态,等这个线程执行完毕后,主线程开始恢复正常,继续往下执行

sleep方法
Thread.sleep(1000):睡眠一秒钟
就是1秒钟后进入就绪状态,等待系统分配CPU资源

Thread.sleep(0):立刻进入就绪状态,等待系统分配CPU资源,类似于yield方法

wait、notify 、notifyall

1、wait()、notify/notifyAll() 方法是Object的本地final方法,无法被重写。

2、wait()使当前线程阻塞,前提是 必须先获得锁,一般配合synchronized 关键字使用,即,一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法。

3、 由于 wait()、notify/notifyAll() 在synchronized 代码块执行,说明当前线程一定是获取了锁的。
当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。
只有当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。
也就是说,notify/notifyAll() 的执行只是唤醒沉睡的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程让其获得锁

4、wait() 需要被try catch包围,以便发生异常中断也可以使wait等待的线程唤醒。
5、notify 和wait 的顺序不能错,如果A线程先执行notify方法,B线程在执行wait方法,那么B线程是无法被唤醒的。

6、notify 和 notifyAll的区别
notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。

interrupt()、interrupted():中断、复位
如果我们要结束线程怎么操作?
1.调用stop()方法(不推荐使用),会在run方法未执行完毕时,直接跳出去

2.定义一个共享变量,默认false,需要停止时,将值改为true

3.调用interrupt方法(当run方法有循环或者该线程阻塞状态时使用)
循环
本质jvm源码有个属性interrupt,默认为false,当调用interrupt方法时,此值改为true
通过Thread.currentThread().isInterrupted()获取属性值

阻塞状态
调用interrupt方法,唤醒此线程,然后抛出interrupt异常
比如一个线程sleep的很久,如果不结束,就意味着性能消耗,所以不可能无限sleep,可以调用interrupt方法,唤醒此线程,并抛出异常,结束此线程

interrupted()复位是,将interrupt属性值恢复为默认值

6.Synchronized介绍

Synchronized作用:

对于普通方法: 锁住的是当前实例对象
对于静态方法: 锁住的是当前类的class对象
对于静态代码块: 锁住的是括号里面的配置对象

JDK1.6以后锁优化:
在jdk1.6之前,【synchronized】是一直都被称为重量级锁;但是在jdk1.6之后,【synchronized】进行了各种优化

1.自适应自旋锁
自旋锁
自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占有处理器的时间,如果锁被占有的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。

自适应自旋锁
自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时,将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

2.偏向锁、轻量级锁、重量级锁(这里只做简单介绍)

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁

轻量级锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

3.锁消除、锁粗化
锁消除
锁消除,即去除不可能存在共享资源竞争的锁。
众所周知,【StringBuffer】是线程安全的,因为内部的关键方法都是被synchronized修饰过的,
但是上述代码中,sb是局部变量,不存在竞争共享资源的现象,此时JVM会自动需要【StringBuffer】中的锁。

锁粗化
通常我们为了降低锁粒度,会选择第一种加锁方式,仅在线程共享资源处加锁,从而使得同步需要的操作数量更少。
而锁粗化思想,就是扩大加锁范围,避免反复加锁

7.线程安全

保证线程安全

1.原子性:
这一点,跟数据库事务的原子性概念差不多,即一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)。
  关于原子性,一个非常经典的例子就是银行转账问题:比如A和B同时向C转账10万元。如果转账操作不具有原子性,A在向C转账时,读取了C的余额为20万,然后加上转账的10万,计算出此时应该有30万,但还未来及将30万写回C的账户(不具备原子性),此时B的转账请求过来了,B发现C的余额为20万,然后将其加10万并写回。然后A的转账操作技术——将30万写回C的余额。这种情况下C的最终余额为30万,而非预期的40万。

2.可见性

可见性是指,当多个线程并发访问共享变量时,一个线程共享变量的修改,其它线程能够立即看到,保证数据的实时性。比如A线程将共享变量改为3时,其他线程在使用这个共享变量时,获取的值是3,而非其他值

3.顺序性

顺序性指的是,程序执行的顺序按照代码的先后顺序执行。
从代码顺序上看,上面四条语句应该依次执行,但实际上JVM真正在执行这段代码时,并不保证它们一定完全按照此顺序执行。
  处理器为了提高程序整体的执行效率,可能会对代码进行优化,其中的一项优化方式就是调整代码顺序,按照更高效的顺序执行代码。

解决线程安全问题多线程并发问题

1.Java如何保证原子性
锁和同步
常用的保证Java操作原子性的工具是锁和同步方法(或者同步代码块)。使用锁,可以保证同一时间只有一个线程能拿到锁,也就保证了只一时间只有一个线程能执行申请锁和释放锁之间的代码。

2.Java如何保证可见性
Java提供了 volatile 关键字来保证可见性。当使用volatile修饰某个变量时,它会保证对该变量的修改会立即被更新到内存中,并且将其它缓存中对该变量的缓存设置成无效,因此其它线程需要读取该值时必须从主内存中读取,从而得到最新的值。

3.Java如何保证顺序性
  上文讲过编译器和处理器对指令进行重新排序时,会保证重新排序后的执行结果和代码顺序执行的结果一致,所以重新排序过程并不会影响单线程程序的执行,却可能影响多线程程序并发执行的正确性。
  synchronized和锁保证顺序性的原理和保证原子性一样,都是通过保证同一时间只会有一个线程执行目标代码段来实现的。

猜你喜欢

转载自blog.csdn.net/weixin_42371621/article/details/109527047