线程

多线程程序:可以同时运行一个以上线程的程序.

多进程和多线程的区别:多线程共享数据,多进程每个进程拥有自己的一整套变量.

使用线程

当需要执行一个比较耗时的任务时应当并发的运行任务

  1. 将任务放入实现了runnable接口的类的run方法中(Runnable是函数式接口,可以使用lambda表达式建立一个实例)
public interface Runnable{
    void run();
}
//lambda
Runnable r=()->{task code;};
  1. 由runnable创建一个thread对象
Thread t= new Thread(r);
  1. 启动线程
t.start();

警告:不要调用thread类或runnable类的run方法,直接调用run还是在原线程中执行任务,应该调用thread.start方法,这将创建一个执行run方法的新线程

中断线程

线程终止条件,无法强制线程终止.

  • 当run方法执行最后一条语句并由return语句返回时
  • 出现了没有捕获的异常时
  • 调用了stop方法时(早期存在,现被启用)

interrupt方法可以请求终止线程,调用该方法后线程的中断状态将被置位.线程会时不时检查该boolean标志,以判断是否被中断.

注意: 如果每次工作迭代后调用sleep方法,isInterrupted检测将会失去作用,在中断状态被置位时调用sleep方法,线程不会休眠.而是会清除置位状态(!)并抛出InterruptedException异常.因此若循环调用sleep则不用检测标志位,而需要捕获异常

注意:不要将InterruptedException异常抑制在很低的层次上,最好用throws InterrupetedExcption标记方法,抛出异常给调用者

线程状态

线程可以有六种状态:

  • new(新创建)
  • Runnable(可运行)
  • Blocked(被阻塞)
  • waiting(等待)
  • timed waiting(计时等待)
  • terminated(被终止)

getState方法可以获得线程状态.

  • new一个线程后 进入新创建状态
  • 调用start后 线程处于runnable状态,可运行状态可能运行也可能没运行,取决于操作系统给线程提供运行的时间

抢占式调度会给每个线程分配时间片

在多处理器机器上,每一个处理器运行一个线程,可以有多个线程并行运行,若线程数大于处理器数仍会采用时间片机制

非抢占式调度.只有线程调用yield或被阻塞,等待时才会失去控制权

  • 线程试图获得锁且锁被其他线程持有时进入阻塞状态
  • 等待另一个线程通知调度器一个条件时进入等待状态
  • 调用sleep,wait,join,trylock等带有超时参数方法后,线程进入计时等待状态
  • 终止线程
    • 因run方法正常退出自然死亡
    • 因没有捕获的异常终止run方法意外死亡

线程属性

线程优先级

默认情况,线程继承父线程的优先级.可以使用setPriority方法提高或降低任何一个线程的优先级,优先级可以设置为1到10.默认为5.

守护线程

调用t.setDaemon(true);将线程转换为守护线程,守护线程的作用是为其他线程提供服务.如:计时线程

当只剩下守护线程时,虚拟机就退出.

守护线程不应该访问固有资源,因为它会在任何时间甚至操作中间发生中断

未捕获异常处理器

run方法不能抛出任何受查异常,但是非受查异常会导致线程终止.但是不需要catch子句来处理可以被传播的异常,可以在线程死亡之前,传递异常到一个用于未捕获异常的处理器

处理器是实现了thread.UncaughtExceptionHandler接口的类

//该接口只有一个方法
void uncaughtException(Thread t,Throwable e)

可以使用setUncaughtExceptionHandler方法为任何线程安装一个处理器.也可以使用thread类的静态方法setDefaultUncaughtExceptionHandler为所有线程安装一个默认处理器

同步

竞争条件: 多个线程同时存取相同的对象,可能产生错误的对象

锁对象

  • synchronized关键字
  • ReentranLock类

使用reentranLock保护代码块

myLock.lock();
try{
    …
}finally{
    mylock.unlock;
}

该结构保证任何时刻只有一个线程进入临界区,一旦一个线程封锁了锁对象,其他线程都无法通过lock语句.其他线程调用lock时会被阻塞直到第一个线程释放锁对象.

警告:将解锁操作放在finally相当重要,如果临界区代码抛出异常,也必须释放锁,不然其他线程永远阻塞

注释:使用锁,就不能使用带资源的try语句

锁可重入,线程可以重复获得已持有的锁,锁保持一个持有计数,因此,被一个锁保护的代码可以调用另一个使用相同的锁的方法.

条件对象

线程进入临界区却发现在一条件满足后才能执行

以银行转账为例
假如给临界区加锁,会导致进入转账操作却发现余额不足,而此时又因为锁的关系,其他线程无法给账户增加余额.

因此我们需要条件对象,使用newCondition方法获得一个条件对象.如果转账方法发现余额不足,它会调用Conditon.await()方法,阻塞该线程并放弃锁,线程进入该条件等待集.直到另一个线程调用同一条件上的signalAll方法.该方法会通知此条件集所有线程,使之再次称为可运行状态,一旦锁可用,这些线程将从await调用返回并从阻塞的地方重新执行

通常,对await的调用应该在如下形式的循环体中

while(!(ok))
    condition.await();
signalAll方法:

应该在对对象的状态有利于等待线程的检测结果改变是调用signalAll方法,使等待的线程有机会重新检测.

public void f(){
    lock.lock();
    try{
        while(?){
             conditon.await();
        ….
        condition.signalAll();
        }
    }finally{
        lock.unlock();
    }
}
死锁:

当一个线程调用await后寄希望于其他线程激活,如果没有其他线程来激活等待的线程他就永远不再运行了.

signalAll方法并不能立即激活等待线程,只是解除阻塞,以便这些线程可以在当前线程退出同步方法后通过竞争,实现对对象的访问.

另一个signal方法,高效但比较危险,随机解除等待集中某个线程的阻塞,如果随机调用的线程发现自己仍然不能运行,它将再次阻塞,如果没有其他线程再次调用signal,系统就死锁了

总结:

  • 锁可以用来保护代码片段,任何时刻只有一个线程执行被保护的代码
  • 锁可以管理试图进入被保护代码段的线程
  • 锁可以拥有一个或多个相关的条件对象
  • 每个条件对象管理哪些已经进入被保护代码段但是还不能运行的线程

synchronized关键字

Lock和Condition接口提供了高度的锁定控制,但大多数情况不需要这样的控制.因此可以使用synchronized关键字声明.使用synchronized可以使代码变得简洁,而且该锁有内部条件管理试图进入方法的线程.

  • 用synchronized声明的方法,会被对象的内部所保护.
public synchronized void method(){
    body
}
  • 内部对象锁可用wait方法添加一个线程到等待集中,notifyAll/notigy方法将解除等待线程的阻塞状态.
  • 可以将静态方法声明为synchronized,获得的是类对象的内部锁,会禁止其他线程调用该类的静态方法.

内部锁和条件存在的局限

  • 不能中断一个正在试图获得锁的线程
  • 试图获得锁时不能设定超时
  • 每个锁仅有单一的条件,可能是不够的

lock和condition对象还是同步方法的选择建议:

  • 最好都不使用,可以使用java.util.concurrent包中的机制
  • 如果可以使用synchronized.尽量使用,可以减少代码量
  • 特别需要lock/condition的独有特性时才使用它

同步阻塞

不只可以通过调用同步方法获得锁,还可以使用客户端锁定

synchronized (obj)
{
    …
}

它将获得obj的锁,但是为了达到目标必须保证obj的所有可修改方法都是同步的,因此客户端锁定非常脆弱,通常不建议使用

监视器概念

由于缩合条件不是面向对象的,研究人员逸之在寻找不需要程序员考虑如何加锁的情况下保证多线程安全性的办法,最成功的解决方法之一就是监视器.

监视器的特性

  • 监视器是只包含私有域的类
  • 每个监视器类的对象有一个相对的锁
  • 使用该锁对所有方法加锁,则调用方法是自动获得对象的锁,并且在方法返回时自动释放锁.
  • 该锁可以有任意多个相关条件
    java并不精确的使用了监视器概念,每一个对象有一个内部锁和一个内部条件,方法使用了synchronized声明则表现得像监视器方法.

在以下三个方面Java对象不同于监视器是的线程安全性下降.

  • 域不要求必须是private
  • 方法不要求必须是synchronized
  • 内部锁对客户是可用的

Volatile域

在读写一两个实例域时为什么要使用开销很大的同步,为什么会出错?

  • 多处理器计算机可能会在寄存器或本地内存缓冲区中保存内存中的值,因此运行在不通处理器的线程可能在同一个内存位置取到不通的值
  • 编译器可以改变指令执行顺序使吞吐量最大化,顺序变化不会改变代码的语义,编译器假定只有在代码中有显式的修改指令时才会修改内存中的值,但是实际上内存中的值可以被其他线程改变
    使用锁则可以避免这种问题,编译器被要求通过必要时刷新本地缓存保持锁的效应,且不能不正当重新排序指令

volatile关键字为实例域提供免锁机制,如果声明为volatile,则编译器和虚拟机会知道该域可能被另一个线程并发更新

警告,volatile变量不能提供原子性.如方法f(){volatile=!volatile;}无法确保反转volatile域中的值,不能确保读取,反转和写入不被中断.

final变量

除了使用锁和volatile外还可以使用final来安全的访问一个共享域

final Map<string,double> accounts=new HashMap<>();

其他线程会在构造函完成后才看到这个变量.不使用final就不能保证其他线程看到的是更新后的值,他们有可能可能看到的是null而不是一个hashMap

当然 对这个映射表的操作仍不是线程安全的,多个线程在读写的话仍然需要同步

原子性

volatile可以保证赋值操作的原子性,而需要其他操作的原子性时,可以使用java.util.concurrent.atomic包. 有很多方法可以以原子方式设置值.但是如果希望完成较为复杂的更新,就需要使用compareandeset方法,假如希望跟踪不同线程观察的最大值,下面的代码是不能完成的

public static void Atomiclong largest = new AtomicLong();
largest.set(Math.max(lagest.get(),observed));

应当在一个循环中计算新值和使用compareAndSet

do {
    oldvalue=largest.get();
    newvalue=math.Max(oldvalue,observed);

}while(!compareandeset(oldvalue,newvlaue));

如果另一个线程也在更新largest,就可能阻止这个线程更新,compareandeset就会返回false,而不会设置新值.循环就会再次尝试,最终他会成功用新值替换原来的值.但是因为他的方法映射一个处理器操作,所以比使用锁速度更快.

java SE 8 中不再需要编写这样的循环代码.实际上可以提供一个lambda表达式更新变量,他会为你完成更新.对于这个例子我们可以

largest.updateAndGet(x->Math.max(x,observed));
或
largest.accumulateAndGet(oberved,Math::max);

accumlateAndGet方法使用一个二元操作符来合并原子值和提供的参数.

如果有大量线程要访问相同的原子值,性能会大幅下降,因为乐观更新需要太多次重试,java SE8提供了longAdder和LongAccumulator类来解决这个问题.

死锁

锁和条件不能解决多线程中的所有问题

死锁 就是每一个线程都在等待其他线程完成操作而导致所有线程被阻塞.

线程局部变量

使用ThreadLocal辅助类为各个线程提供各自的实例.

比如要构建局部SimpleDateFormat对象

public static final ThreadLocal<SimpleDateFormat> dateFormat = Threadloacal.withInitial(()->new SimpleDateFormat("yyyy-mm-dd"));
//要访问具体的格式化方法
String dateStamp=dateFormat.get().format(new Date());

在一个给定线程中首次调用get时,会调用initialValue方法.在此之后,get方法会返回属于当前线程的那个实例

锁测试与超时

<wiz_tmp_tag id="wiz-table-range-border" contenteditable="false" style="display: none;">





原文地址:https://www.cnblogs.com/renluxiang/p/21b79cdd4ef53366de7806f473583a2f.html