多线程的使用01

1 为什么使用多线程

  1.1 发挥多核cpu的优势

    单核CPU上的多线程是假的多线程,同一时间处理器只会处理一段逻辑,只是在多个线程之间进行快速切换

    多核CPU才能实现真正的多线程,同时处理多个逻辑,充分利用CPU

  1.2 防止阻塞

    单核CPU不仅不能发挥多线程的优势,反而因为多个线程的切换,反而降低整体效率。

    但是单核CPU还是要使用多线程,防止阻塞。这样在一个线程卡死以后,不会影响其他线程的正常运行。

  1.3 便于建模

    对于一个大的而且复杂的任务,可以分为多个小的简单的任务进行程序建模。

2 start()和run()方法的区别

  start()方法来启动线程,真正实现多线程。无需等待run()方法体的执行而直接执行下面的代码。

    通过调用Thread来的start()方法启动一个线程,此时该线程处于就绪(可运行)状态,但并没有运行,一旦得到cpu时间片,就开始执行run()方法,称为线程体。

  run()只是个普通的方法,并不会创建新的线程也不会执行调用线程的代码。

3 CyclicBarrier和CountDownLatch的区别

  3.1 CyclicBarrier在某个线程中调用await()方法后,该线程就停止运行,知道所有的线程到到进入到barrier状态,执行完CyclicBarrier中定义的run()方法后再执行该线程之后的代码。

    而CountDownLatch,当线程运行到await()时,只要CountDownLatch中的数值减到0,该程序就会继续运行。

  3.2 CyclicBarrier的await()只是让当前线程停止运行,而CountDownLatch的await()会让所有的线程都进入停止运行

  3.3 CyclicBarrier可重复使用,而CountDownLatch不可重复使用

public static void main(String[] args) {
        
        /**
         * CountDownLatch 的三个重要方法
         * await() :调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
         * await(long timeout, TimeUnit unit) :和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
         * countDown() :将count值减1,如果计数到达零,则释放所有等待的线程。
         */
        //定义一个计数器,并设置计数器的初始值
        final CountDownLatch latch=new CountDownLatch(2);
        
        /**
         * 通过它可以实现让一组线程等待至某个状态之后再全部同时执行。
         * 叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。
         * 我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了。
         */
        final CyclicBarrier barrier=new CyclicBarrier(2, ()->{
            System.out.println("所有到子线都进入到了barrier状态");
        });
        
        for(int i=0;i<2;i++){
            //定义一个子线程
            new Thread(()->{
                System.out.println("子线程"+Thread.currentThread().getName()+"开始运行");
                try {
                    Thread.sleep(3000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println("子线程"+Thread.currentThread().getName()+"运行结束");
                
                //将计数器的值减1
                latch.countDown();
                System.out.println("子线程"+Thread.currentThread().getName()+"减一之后");
                
                try {
                    Thread.sleep(2000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                
                System.out.println("子线程"+Thread.currentThread().getName()+"进入到barrier");
                try {
                    //让该线程进入到barrier状态,暂停运行,等所有的线程都进入到barrier状态后,
                    //执行barrier中定义的run()方法后运行之后的额代码
                    barrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println("子线程"+Thread.currentThread().getName()+"barrier之后的代码");
                
                
            }).start();
        }
        
        System.out.println("主线程"+Thread.currentThread().getName()+"开始运行");
        try {
            System.out.println("等待两个子线程运行结束");
            
            //将主线程挂起,等待计算器的值变为0后再向下执行
            latch.await();
            
            //将主线程挂起,等待2000毫秒,如果计数器的值还没有变为0,就向下执行
            //latch.await(2000,TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("两个子线程运行结束");
        System.out.println("主线程"+Thread.currentThread().getName()+"运行结束");
    }

4 volatile关键字

  4.1 并发编程的三个概念:原子性,可见性和有序性

    4.1.1 原子性:即一个或多个操作要么全部执行且执行的过程不会被任何因素打断,要么就都不执行。

    4.1.2 可见性:指多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程就能立即看到修改的值。

    4.1.3 有序性:程序执行的顺序按照代码的先后顺序执行。

  4.2 java语音本身对并发编程的保证

    4.2.1 原子性

      在java中对基本数据类型的变量的读取和赋值操作是原子性操作的(简单的读取和赋值,变量间的赋值不是原子性的)

      java1.5之前可用通过Syschronized和Lock来实现,java1.5之后可以通过java.util.concurrent.atomic包下提供了一些原子操作类实现

    4.2.2 可见性

      通过volatitle实现可见性:

        被volatitie修饰的变量修改后会立即更新到主存中,其他线程需要读取时会从主存中读取

        而普通变量不能保证可见性的原因是,修改后什么时候更新到主存中是不确定的

      通过Syschronized和Lock保证可见性

        保证同一时刻只有一个线程获取锁然后执行同步代码,并在释放锁之前将变量的修改刷新到主存中

    4.2.3 有序性

      在java内存模型中,允许编译器和处理器对指令进行重排序,但重排序不会影响单线程程序的执行,却会影响到多线程并发执行的正确性。

      volatitle可以保证一定的有序性,但要保证完整的有序性还是需要使用Syschronized和Lock来实现

      java内存具有一些先天的有序性,即不通过任何手段就能保证的有序性,称为“happens-before原则”(先行发生原则)

  4.3 volatitle对并发编程三特性的影响

    4.3.1 可见性 保证了不同线程对共享变量的可见性

    4.3.2 原子性 volatitle不能保证对变量的任何操作都是原子性的

    4.3.3 有序性 禁止了指令重排序

      当执行到volatitle变量的读取和写操作时,其前面的操作肯定是全部执行完了,且结果已经对后面的操作可见,而在此后面的代码肯定没有执行到

  4.4 volatitle使用条件:保证原子性操作,才能保证使用volatitle关键字的程序在并发时能够正确执行

    4.4.1 对变量的写操作不依赖于当前值

    4.4.2 该变量没有包含在具有其他变量的不变式中

5 sleep()和wait()的区别

  两者都可以让程序放弃CPU一段时间,不同点在于如果线程持有某个对象的监视器,sleep不会放弃这个对象的监视器,而wait()会放弃这个对象的监视器。

6 ThreadLocal的作用

  使用ThreadLocal维护变量时,ThreadLocal为每个使用改变了的线程提供独立的变量副本,所以每一个线程都可以独立的改变自己的副本,而不影响其他线程中的副本。

7 wait()和notify()/notifyAll()在放弃对象监视器的区别

  wait()会立即释放对象监视器

  notify()/notifyAll()会等待线程剩余代码执行完毕后才会释放对象监视器

Integer lock=3;
synchronized (lock) {
    //仅当对象obj的监视器被当前线程持有的时候才会返回true
    boolean b=Thread.holdsLock(lock);
    System.out.println("该线程拥有该对象监视器:"+b);
}

8 死锁

Integer a=3;
        Integer b=4;
        new Thread(()->{
                synchronized (a) {
                    System.out.println("线程"+Thread.currentThread().getName()+"获取到a锁,正在等待b锁");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (b) {
                        System.out.println("线程"+Thread.currentThread().getName()+"获取到b锁");
                    }
                }
        }).start();
        
        new Thread(()->{
                synchronized (b) {
                    System.out.println("线程"+Thread.currentThread().getName()+"获取到b锁,正在等待a锁");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (a) {
                        System.out.println("线程"+Thread.currentThread().getName()+"获取到a锁");
                    }
                }
        }).start();
    }

  8.1 死锁问题定位

    8.1.1 jps获取当前虚拟机进程的pid

      

    8.1.2 jstack 打印堆栈信息

      

      

      两个线程各自持有对方在等待的锁,故而造成死锁。

    8.1.3 使用taskkill 命令结束该进程

      

  8.2 避免死锁的方法

    8.2.1 让程序每次至多只能获得一个锁

    8.2.2 尽量减少嵌套的加锁

    8.2.3 通过使用Lock类的tryLock方法去尝试获取锁,这个方法可以指定超时时间,超时后返回失败信息并释放锁。

9 Thread.sleep(0)的作用

  由于java采用抢占式的线程调度算法,为了让优先级比较低的线程也能获取到CPU控制权,使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,来平衡CPU控制权。

    

原文地址:https://www.cnblogs.com/lifeone/p/7872313.html