JAVA多线程知识点

JAVA多线程知识点汇总

1.创建多线程有几种方式?

  • 继承Thread类
  • 实现Runnable接口
  • 应用程序可以使用Executor框架来创建线程池

2.线程状态:

  新建-就绪-运行-阻塞-死亡。

3.同步方法和同步代码块的区别是什么?

  • 同步方法默认用this或者当前类class对象作为锁;

  • 同步代码块可以选择以什么来加锁,比同步方法要更细颗粒度,我们可以选择只同步会发生问题的部分代码而不是整个方法;

4.死锁概念:

  两个线程或两个以上线程都在等待对方执行完毕才能继续往下执行,结果就是这些线程都陷入了无限的等待中。

  多线程产生死锁的四个必要条件:破坏其中一个既可避免死锁或解决死锁。

  • 互斥条件:一个资源每次只能被一个进程使用。

  • 保持和请求条件:一个进程因请求资源而阻塞时,对已获得资源保持不放。

  • 不可剥夺性:进程已获得资源,在未使用完成前,不能被剥夺。

  • 循环等待条件(闭环):若干进程之间形成一种头尾相接的循环等待资源关系。

5.volatile关键字

  它保证的是有序性和可见性!!!注意没有原子性!!!!只修饰变量,是轻量级实现。这也是和synchronized主要区别。用volatile修饰的变量,每次读取时都会获取修改后(最新)的值进行操作(即可见性)。读取数值时必须重新从主内存加载,并且read load是连续的。修改共享变量后,必须马上同步回主内存,并且存储和写入是连续的。但是没有锁机制无法做到线程安全。更适合用于一个修改者,多个使用者,如状态标识,数据定期发布场景。

  代码样例:不加volatile,其他线程读取不到当前线程的最新值

public class VolatileKey extends Thread {
    volatile boolean flag = false;

    public void run() {
        while (!flag) {
            System.out.println("run");
        }

        System.out.println("stop");
    }

    public static void main(String[] args) throws Exception {
        VolatileKey vt = new VolatileKey();
        vt.start();
        Thread.sleep(2000);
        vt.flag = true;
    }
}

  造成这种问题的原因主要是JVM对内存分配的优化,不加volatile时,线程会保存副本,而不是每次都从主内存获取。而volatile限制线程不进行内部缓存和重排,既而解决掉可见性问题。

6、CountDownLatch

CountDownLatch是一个计数器闭锁,通过它可以完成类似于阻塞当前线程的功能,即:一个线程或多个线程一直等待,直到其他线程执行的操作完成。

CountDownLatch用一个给定的计数器来初始化,该计数器的操作是原子操作,即同时只能有一个线程去操作该计数器。当计数器为0时执行await()方法后的代码。

用法:

1.在线程结束时调用.countDown()方法。

2.在主线程合适位置调用.await()方法。

样例代码:

public class CountDownLatchTest {
    public static void main(String[] args) {
        final CountDownLatch latch = new CountDownLatch(2);
        System.out.println("主线程开始执行…… ……");
        //第一个子线程执行
        ExecutorService es1 = Executors.newSingleThreadExecutor();
        es1.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                    System.out.println("子线程:"+Thread.currentThread().getName()+"执行");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                latch.countDown();
            }
        });
        es1.shutdown();

        //第二个子线程执行
        ExecutorService es2 = Executors.newSingleThreadExecutor();
        es2.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("子线程:"+Thread.currentThread().getName()+"执行");
                latch.countDown();
            }
        });
        es2.shutdown();
        System.out.println("等待两个线程执行完毕…… ……");
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("两个子线程都执行完毕,继续执行主线程");
    }
}

7、CyclicBarrier 

作用是设置栅栏阻挡所有线程,当所有线程都完成后才进行后续操作。可循环利用,且提供reset方法重置。

用法:

1.在线程内调用.await()方法,CyclicBarrier会在所有线程都将await前的任务完成时,才继续执行后面的代码(本线程内的)。可以在一个方法内多次调用.await()。

2.和countDownLatch的区别:countDownLatch是减法计数器cyclicBarrier是加法计数器。cyclicBarrier可复用,即一个函数可调用多次.await()方法,且提供rest()函数进行主动重新计数。countDownLatch是一次性的。

8、Semaphore

Semaphore用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。还可以用来实现某种资源池限制,或者对容器施加边界。比如限制最大5个人同时访问。

用法:

1.Semaphore semp = new Semaphore(5);//创建通行5个线程

2.semp.acquire();//发放通行证

3.semp.release();//释放资源

当同时访问线程数大于等于5个时,会阻塞,达到限流目的。

9、有关yield()

功能为:暂停当前正在执行的线程对象,并执行其他线程。

注意:yield()让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会,但是不保证一定让其他线程执行,因为有可能会被再次选中。

所以yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

10.简单介绍线程未捕获异常处理

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

/**
 *异常线程
 */
class ExceptionThread implements Runnable {
    @Override
    public void run() {
        System.out.println("UncaughtExceptionHandler " + Thread.currentThread().getUncaughtExceptionHandler());
        System.out.println("thread " + Thread.currentThread());
        throw new RuntimeException();
    }
}

/**
 * 未捕获异常处理。
 */
class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("catch thread = " + t);
        System.out.println("catch ex = " + e);
    }
}

/**
 *利用线程池获取线程时使用的工厂类
 */
class MyThreadFactory implements ThreadFactory {
    @Override
    public Thread newThread(Runnable r) {
        System.out.println("runnable " + r);
        Thread thread = new Thread(r);
        //设置未捕获异常处理类
        thread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        return thread;
    }
}

public class ExceptionThreadMain {
    public static void main(String[] args) {
        ExecutorService ex = Executors.newCachedThreadPool(new MyThreadFactory());//指定工厂
        ex.execute(new ExceptionThread());
        ex.shutdown();
    }
}

运行结果可对比查看

runnable java.util.concurrent.ThreadPoolExecutor$Worker@6d6f6e28[State = -1, empty queue]
UncaughtExceptionHandler util.threadutil.MyUncaughtExceptionHandler@3986e8ee
thread Thread[Thread-0,5,main]
catch thread = Thread[Thread-0,5,main]
catch ex = java.lang.RuntimeException

JAVA中异常总基类是Throwable,其大部分异常都继承自Exception(如IOException和RuntimeException及其子类)和Error类(如AWTError的StackOverFlowError)

11、Thread、ThreadLocal、ThreadLocalMap

ThreadLocal主要是做到线程间数据隔离,从而达到多线程安全的效果,与Synchronized完全不同。

ThreadLocalMap的代码位置在ThreadLocal里,并且是一个静态内部类。

而每一个Thread里都会有ThreadLocalMap实例,所以TheadLocal里保存变量的集合就是此map。

由于ThreadLocalMap里的entry里的Key部分采取的弱引用,如果ThreadLocal没有强引用指向它,则不会阻止GC回收ThreadLocalMap里Entry的Key部分,但是Value部分却是强引用。就会导致内存泄漏。

所以推荐ThreadLocal采用静态声明或每次用完都调用一次remove()函数。

12、线程杂谈

  1. 如何在多线程之间共享数据:全局变量,静态变量,或共享对象。
  2. 共享变量必须放在主内存中,因为每个线程都有自己的内存区,也只能操作自己的内存区,所以解决可见性问题是从主内存读取到工作内存,操作后写回到主内存中。
  3. 被共享的变量(对象)放到堆、方法区里(主内存)。也就能和jvm的内存分配联系起来。方法区也称"永久代",它用于存储虚拟机加载的类信息、常量、静态变量、是各个线程共享的内存区域。而堆内存存放大多数对象,因为堆内存只有一个,所以是线程共享的。也正是因为如此(从主内存中读,再写回主内存),带来了线程安全问题。
  4. java同步交互协议(从主内存读到工作内存协议)有八个原子操作(注意,操作是原子的,但每个原子操作之间没有原子性)

(1)锁定-主内存变量锁定,线程独享。

(2)解锁-解锁变量,其他线程可访问。

(3)读取-从主内存读取到工作内存。

(4)载入-将读取的主内存值保存到工作内存的副本中。

(5)使用-工作内存使用读取的值。

(6)赋值-工作内存操作读取的值。

(7)存储-从工作内存的值传送给主内存。

(8)写入-将存储的值写入到主内存中。

13、常用的几种线程池

  1.newSingleThreadExecutor

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代再次执行它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

  2.newFixedThreadPool

创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,会新分配线程再次执行。

  3.newCachedThreadPool

创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

  4.newScheduledThreadPool

  创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

posted @ 2021-04-07 21:52  chxLonely  阅读(88)  评论(0编辑  收藏  举报