java多线程
一、线程的概念
要理解线程首先得理解进程,进程就是一个应用运行的过程从代码解析到代码执行再到代码执行完毕一个完整的流程,并发执行的进程数目并不受限与cpu的数目,操作系统为每个进程划分cpu的时间片,给人并行处理的感觉。
线程是指进程中的执行流程,是实现并发的基本单位,进程就由多个线程组成,在实际应用中非常广泛,例如,迅雷下载器同时下载多个任务,在比如网易云听歌的时候边听歌边评论者也是一种多线程
1.1线程状态
1.1.1新建&运行进程
new Tread时线程的状态处于新建状态,在运行之前还有一些基础工作要做
一旦调用start方法,线程就处于可运行状态(可能在运行也可能没有运行)一旦开始运行,它也不一定始终保持运行,需要暂停给其他进程让行。
1.1.2阻塞和等待线程
线程处于阻塞与等待状态时不活动。等待重新激活。
达到非活动状态的几种方式
- 当一个线程尝试获取一个被其他线程占用的内部锁时 ,改线程被堵塞,等待其他线程释放这个锁,调度器允许持有这个锁时,变为非堵塞状态。
- 当线程等待另一个线程通知调度器出现条件时,线程会进入等待状态
- 由几个方法由超时参数时,这些方法会让线程进入记时等待
1.1.3线程的终止(两种原因)
- run方法正常退出
- 以为一个没有捕获的异常终止了run方法,线程意外终止
stop方法已经被废弃,不推荐使用
1.2线程属性
包括中断的状态、守护线程、未捕获异常的处理器以及不应该使用的遗留特性
1.2.1中断线程
stop方法被废弃,只能用interrupt方法来请求终止一个线程
当对一个线程调用interrupt时,会设置线程终止状态,每个线程都应该时不时检查该标志以判断线程是否被中断。
如果线程被阻塞,则无法检查中断状态,当一个线程在wait或sleep中调用interrupt方法时,那个阻塞调用将被一个InterruptedException异常中断
注意区分interrupt方法和isinterrupt方法:interrupt方法调用时检查当前线程是否被中断会清除线程的中断状态,而isinterrupt方法不会改变中断状态
若每次工作迭代之后都调用sleep方法,则没必要使用isinterrupt方法。
1.2.2守护线程
可以通过 setDaemon方法将一个线程转换成守护线程,守护线程唯一用途就是未其他线程提供服务,例如计时器线程。当只剩下守护线程时,虚拟机就会停止运行。
1.2.3线程名
var t = new Thread(runnable);
t.setName("线程名");
1.2.4未捕获异常的处理器
非检查异常可能会导致线程终止,这种情况线程会死亡。
对于可传递的异常,并没有任何catch字句。实际上在线程死亡前异常会传递到一个用于处理未捕获异常的处理器,该处理器需要实现Thread.UncaughtExceptionHandler接口的类。该接口只有一个方法
void uncaughtException(Thread t,Throwable e);
若没为该现场安装该处理器,则处理器就是该线程的TheardGroup对象。
TheardGroup执行操作
- 如果由父线程则执行父线程组的uncaughtException方法
- 否则,如果Thread。getDefaultExceptionHandler方法返回一个非null的处理器,则调用该处理器
- 否则,如果Throwable是ThreadDeath的一个实例,什么都不做。
- 否则,将该线程的名字以及Throwable的栈轨迹输出到System.err。
1.2.5线程优先级
每一个线程都由优先级,可以使用setPriority方法提高或降低如何一个线程的优先级。线程调度器选择线程的时候也会优先选择优先级高的线程。
当虚拟机依赖于宿主机平台的线程实现是,Java线程优先级会映射到平台的优先级。(在没有操作系统线程的java早期版本中,线程优先级可能很有用。现在不要使用Java线程了)
1.3同步
1.3.1竞态条件
先观察下列程序,该程序多个线程在更新银行的账户余额,一段时间后银行总金额发生改变
package threads;
import java.util.Arrays;
public class Bank {
private final double[] accounts;
public Bank(int n,double initalBalance){
accounts = new double[n];
Arrays.fill(accounts,initalBalance);
}
public void transfer(int from,int to,double amount){
if (accounts[from] < amount) return ;
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf("从 %d 转入 %d %10.2f ",from,to,amount);
accounts[to] += amount;
System.out.printf("银行总金额: %10.2f%n",getTotalBalance());
}
public double getTotalBalance(){
double sum = 0;
for (double a : accounts)
sum += a;
return sum;
}
public int size(){
return accounts.length;
}
}
package threads;
public class UnsynchBankTest {
public static final int NACCOUNTS = 100;
public static final double INITIAL_BALANCE = 1000;
public static final double MAX_AMOUNT = 1000;
public static final double DELAY = 1000;
public static void main(String[] args) {
Bank bank = new Bank(NACCOUNTS,INITIAL_BALANCE);
for (int i = 0;i < NACCOUNTS;i++){
int fromAccount = i;
Runnable r = ()->{
try {
while (true) {
int toAccount = (int)(bank.size() * Math.random());
double amount = MAX_AMOUNT * Math.random();
bank.transfer(fromAccount,toAccount,amount);
Thread.sleep((int)(DELAY *Math.random()));
}
}catch (Exception e){
e.getStackTrace();
}
};
Thread t = new Thread(r);
t.start();
}
}
}
上述程序出现问题的原因在于不是原子操作
两个线程存取同一个对象,两个线程分别调用一个修改该对象状态的方法,两个线程会相互覆盖,取决于线程访问数据的次序,可能会导致对象被破坏。这种情况被称为竟态条件。
假设两个线程同时执行指令
accounts[to] += amount
竟态条件出现的问题在于操作不是原子操作:
- 将accounts[to]加载到寄存器
- 增加amount
- 将结果协会accounts[to]
假定第一个线程执行步骤1和2然后运行权被占第二个线程被唤醒更新同一个元素,然后第一个线程完成步骤3。(如图所示)
1.3.2锁对象
旅游两种机制可以禁止并发访问代码块。java语言提供了一个关键字synchronized,另外java5引入了ReentranLock类(重入锁)。synchronized关键字会自动提供一个锁和相关“条件”,对于大多数需要现实所的情况,这个功能很强大。
用ReentrantLock保护代码块的基本结构如下:
myLock.lock();
try{
*critical section*
}
finally{
myLock.unlock();
}
一旦一个线程锁定了锁对象其他线程都无法通过Lock语句
注意:一定要将unlock语句放在finally中。若临界区的代码抛出一个异常锁必须释放。否则线程将永远阻塞
重入锁:该锁可以重复获得已拥有的锁,它完全可以替代 synchronized 关键字来实现它的所有功能,而且 ReentrantLock 锁的灵活度要远远大于 synchronized 关键字。
1.3.3 Condition对象
线程进入临界区后发现只有满足某种条件后才能执行,所以可以使用一个Condition对象来管理那些已经获得了锁却不能做有用工作的线程
//condition对象如何创建
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
condition.await();
condition.signalAll();
//通常调用await()
while(!OK to proceed)
condition.await();
线程调用await方法进入进入对应条件等待集。当锁可用时它仍不会变为可运行状态,直到另一个进程在同一个条件上调用singnalAll方法(通知等待的线程:现在可能有满足条件值得再次检查条件。只是解除了等待进程的堵塞状态。)。当这些线程从等待集中移出时,他们再次成为可运行线程,其中某一个线程会从awit调用中放回,得到这个锁并从之前暂停的地方继续执行,调度器最终会再次将他们激活。
当一个线程调用await方法时它救没有办法自行激活,只能寄希望于其他线程,若没有其他线程重新激活,则出现死锁。
singnal方法:随机选择等待集中的线程,并解除线程内的堵塞状态。
小总结
锁和条件对象
- 锁用来保护代码片段,一次只能由一个线程执行被保护的代码片段
- 锁可以管理视图进入被保护代码段的线程
- 锁可以由一个或多个相关联的条件对象
- 每个条件对象管理那些已进入被保护代码还不能运行的线程
1.3.4synchronized关键字
若将方法声明为synchronized则内部对象锁会保护该方法,内部对象锁只有一个关联条件(Condition)。可以简单的将方法声明为synchronized而不必使用显式锁。
- wait方法:将线程添加到等待集中 相当于intrinsicCondition.await();
- notifyAll方法:解除等待进程的堵塞 intrinsicCondition.singnalAll();
1.3.5同步块(不推荐使用)
synchronized{
*critical section*
}
1.3.6监视器概念
实现线程同步强大的工具,不要求程序员考虑显式锁救可以确保多线程安全性
监视器特点
- 只包含私有字段的类
- 监视器的每个对象都由一个关联的锁
- 所有方法都由这个锁锁定
- 多可以由任意多个相关联的条件
java通过关键字synchronized不太严格的实现了监视器
在以下三方面做出改变,削弱了线程的安全性: - 字段不要就是 private
- 方法不要求是synchronized
- 内部锁对用户可用
1.3.7volatile字段
volatile关键字作用是给一个变量单独设置一个锁,为实例的同步访问提供一个免锁机制,如果声明一个字段为volatile,那么编译器和虚拟机就知道该字段可能被一个线程并发更新。volatile变量不能提供原子性。
1.3.8final变量
使用final关键字修饰一个字段,可以安全的访问一个共享的字段。
1.3.9原子性
假设对共享变量除了赋值以外并不做其他操作,那么可以将这些共享变量声明为volatile。
例如,AtomicInteger类提供了方法incrementAndGet和decrementAndGet,他们分别以原子方式将一个整数进行自增或自减。
1.3.10死锁
因为一个线程的等待,导致所有线程堵塞的情况,称为死锁。所有线程各自占着对方需要的线程,等待对方线程释放自己需要的资源。
1.3.11线程局部变量
使用ThreadLocal辅助类为个线程提供各自的实例
例
- SimpleDateFormat类不是线程安全的。要为每个线程构造一个实例,
public static final ThreadLocal<SimpleDateFormat> dateFomat
= ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd"));