Java基础知识八:多线程基础知识

一、使用最简单的线程执行一个任务:

Thread t = new Thread(new Runnable() {
            @Override
            public void run() {

            }
        });
        t.start();

二、线程的状态 六种

New 新创建:使用 new操作符创建一个线程时,还未运行 处于 new的状态

Runnable 可运行 :当调用start()方法时,线程处于可运行的状态,但实际不一定运行。线程调度的细节依赖于操作系统提供的服务。抢占式调度系统给每一个可运行线程一个时间片来执行任务,当时间片用完,操作系统剥夺该线程的运行权,并给另一个线程机会。当选择下一个线程时,操作系统会考虑线程的优先级。在现在的桌面及服务器操作系统中一般均采用抢占式调度。

Blocked 被阻塞 :当一个线程试图获得一个内部的对象锁时(和 并发库中的锁不同),而该锁被 其他线程所持有时,则 该线程会进入阻塞状态。当其他线程释放该锁,并且调度器允许当前线程获得该锁时,线程 变为非阻塞状态。

Waiting 等待 :当线程因为某个条件不满足时,可以进入 等待状态。

Timed waiting 计时等待 :有些方法,例如 Thread.sleep  Lock.tryLock方法,带有超时的参数信息,可以 计时等待。

Terminated  被终止 :两个原因导致线程的终止:1、因为run方法正常退出而导致的自然终止 2、因为run方法中产生了一个没有捕获的异常而导致 run方法意外被打断

三、线程的优先级

线程的优先级高度依赖于当前的操作系统,不应当过度使用线程的优先级。

四、守护线程

可以 对一个线程对象 调用 t.setDaemon(true)方法,设置当前线程为 守护线程。守护线程的唯一用途是 为 其他线程提供服务。当只剩下守护线程时,虚拟机就退出了,因为 这通常代表没有任务继续执行了。

五、未捕获的异常处理器

run方法不能抛出任何 已检查的异常,但是 未知的异常情况会导致线程的终止。 我们无需使用catch子句来 捕获未知的异常,取而代之的是 在 线程因为 未知的异常退出之前,会将异常传递给 一个 用于为捕获异常的处理器, 当然 作为异常处理器 必须实现 Thread.UncaughtExceptionHandler接口。 

可以对 线程对象 使用 setUncaughtExceptionHandler方法设置 这个对象的异常处理器,也可以 使用静态方法Thread.setDefaultUncaughtExceptionHandler 为 所有的类 安装一个默认的 异常处理器。 如果 二者都不设置,则 此时的异常处理器就是 该线程的 ThreadGroup对象。

六、同步

多线程并发读写同一数据带来的问题:例如 a = 1; a += 1; 的操作。多个线程执行这段代码会导致计算误差,因为这个操作并不原子,在执行时 可能会被拆分为 三个指令: 1、取出a的值 2、执行 加 的操作 3、将 结果赋值给 a 。假设有两个线程同时访问, A线程 执行了 1、2两个步骤,之后阻塞了, 然后 B线程 执行了 1、2、3三个操作,将a的值 变成了 2, 之后 A线程结束了阻塞状态,继续执行了 3操作,也是将a的值变成了 2。 问题出现了: 两个线程执行了两次 +1 操作,结果本该是 3,实际确是 2。 

上述问题的原因在于:A线程的赋值操作 覆盖了B线程的赋值操作。解决的方案之一是 对相应的 操作 加 锁。

七、锁对象

synchronized关键字 和 ReentrantLock类。

ReentrantLock使用的大致方式:

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

一旦某个线程进入临界区获得了锁对象,其他的任何线程都无法通过 lock语句,当其他线程调用lock语句试图获得锁时,会被阻塞,直到锁对象得到释放。  显而易见的是,一定要使用finally语句确保 锁对象在任何情况下都得到释放。

每个对象都会有自己的 锁实例,当多个线程访问同一个对象时,加锁的方法会以串行的方式运行。而 两个线程访问两个对象时,相互之间不会有影响。

ReenTrantLock类型的锁是可以重入的,意思是  进入lock块之后,执行的 methodWithLock方法内部也需要获得锁。当线程已经持有了某个锁之后,该线程可以重复获得已有的锁。这个特性使得,被一个锁保护的代码可以 调用 另一个用相同的锁的方法。期间,锁 会有一个 持有计数来跟踪 lock方法的嵌套调用。每次调用lock方法 都应该使用 unlock方法来释放 锁。

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

八、条件对象

当线程进入临界区之后,发现 当前的条件不满足执行具体代码的条件时,可以使用条件对象的 await()方法来使得当前线程进入阻塞装填,并 放弃 持有的 锁。将机会让给其他线程。其他线程适时使用 signalAll()方法来重新激活因为 这个条件而阻塞的线程。 此处很容易产生死锁:某个线程因为条件不足而进入阻塞状态,此时 它唯一的希望就是 其他线程调用 对应条件的 signalAll()方法,而如果其他线程没能调用此方法,这个线程就进入 死锁状态。

一个锁可以有 多个条件对象。

九、synchronized关键字

从Java1.0开始,每个对象都会有一个 内部锁,synchronized关键字使用 对象的内部锁。使用synchronized关键字修饰的方法,当线程进入此方法时,会自动的获得当前对象的内部锁,当其他线程企图进入 有synchronized关键字修饰的 方法时,会进入阻塞状态。

十、Lock Condition or synchronized?

1、最好都不使用,不要手动加锁。在多数情况下 应该使用 并发包中的 一种机制,会为你处理所有的加锁。

2、优先使用  synchronized关键字

3、特别需要时,才使用 Lock/Condition

十一、volatile域

如果某个实例域可能会被 并发访问,为了确保安全,需要将 访问、修改方法使用 synchronized关键字修饰,但是这样做 开销过大了。 

volatile关键字 为 实例域的同步访问 提供了一种 免锁机制

十二、线程局部变量

有些类,例如 SimpleDateFormat线程不安全,即多线程的访问可能会破坏其内部结构,此时不能声明为static final,但是 需要的时候构造局部变量又太浪费了。 有些类 例如 Random是 线程安全的,但是 声明为 static final 多个线程共享一个对象 又是的效率很低。

此时,可以使用 ThreadLocal辅助类,构造线程局部变量,每个线程都会有 一份自己的实例。

十三、冲入读写锁

ReenTrantReadWriteLock类 可以应用于 大量读取操作少量写入操作的类型。

构造一个对象,然后 抽出 读锁和写锁,之后 读操作加 读锁,写操作 加 写锁。读者线程可以共享访问,而 写者线程必须 互斥访问。

十四、线程安全的集合

concurrent包中 提供了 映射表、有序集、队列的高效实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet。这些集合使用搞笑的算法通过允许并发地访问 数据结构的不同部分来使竞争极小化。

十五、Callable和 Future

前面说,Runnable接口封装了一个 异步执行的任务,没有参数和返回值。Callable类似,但是有 返回值。

Future对象保存异步计算的结果,启动一个异步计算之后,可以通过Future对象拿到计算之后的结果。

十六、线程池

不要手动创建线程,因为涉及到 系统资源的分配。应当使用 线程池,一个线程池中包含许多空闲线程,当将 Runnable对象交给线程池时,会有一个线程来 执行run方法,当run方法执行完成之后,线程不会死亡,而是 在池中准备下一次服务。

Executors类有许多静态工厂方法用来构建线程池。可以使用newFixedThreadPool创建有固定大小的线程池,如果提交的任务数多于空闲的线程数,则把得不到服务的任务放置到队列中,当其他任务完成后再运行;newCachedThreadPool方法构建一个线程池,对于每个任务,如果有空闲线程可用,立即让它执行任务,如果没有可用的空闲线程,则创建一个新线程;newSingleThreadExecutor是一个大小为1的线程池:由一个线程执行提交的任务,一个接一个。

创建线程池的静态工厂方法返回一个 实现了ExecutorService接口的ThreadPoolExecutor类的对象,可以使用 对象的submit方法,将Runnable或者Callable对象提交给 ExecutorService,返回Future<>对象,可以获知执行状态和结果。

当用完一个线程池时,调用 shutdown方法,启动线程池的关闭序列,被关闭的执行器不在接收新的任务,当所有任务都完成时,线程池中的线程死亡。

十七、预定执行、定时任务

ScheduledExecutorService接口提供了定时执行或者重复执行的方法,可以通过 Executors类的 newScheduledThreadPool 或者 newSingleThreadScheduledExecutor获取对应接口的对象。定时执行器接口的 schedule 或者 scheduleAtFixedRate/Delay方法指定  周期性运行和 在一次调用完成和下一次调用开始时有 一定长度的延时


猜你喜欢

转载自blog.csdn.net/weixin_37882382/article/details/79796974