并发问题的来历
以前的计算机是只有单个CPU
,并且一次只能执行一个程序,后来出现了多任务处理,但它不是真正的并行处理,单个CPU
是被两个程序共享的,操作系统在两个程序之间进行来回切换。
随着多任务处理的出现,开发软件有着新的挑战,同一个程序不能长时间的占用着所有的CPU
资源、内存、以及任何电脑资源。一个好的程序应该能在不使用时释放所有的资源。
后来出现了多线程,意味着同一个程序可以使用多个线程进行执行,当你在程序中使用多线程时,就像是有多个cpu进行执行一样。
多线程是可以提高某些程序的性能的,然而,多线程是比多任务有更大的难度的,多个线程在同一个程序中执行,对于以前的单个cpu来说是没有问题的,因为总的来说每个线程都还是串行的,但是现代的电脑基本是多核cpu或者是多个芯片,这样执行起来之后就真正的达到了并行,因此对应程序开发更具挑战。
如果一个线程读取一个本地的内存变量,而另一个线程对这个内存进行写,那么这个第一个线程读取到的值是什么呢?
Java 并发学习指南
如果你是Java并发学习新手,建议你按找下面的步骤进行学习。
Java并发和多线程的理论
- Multithreading Benefits
- Multithreading Costs
- Concurrency Models
- Same-threading
- Concurrency vs. Parallelism
Java并发的基础知识
- Creating and Starting Java Threads
- Race Conditions and Critical Sections
- Thread Safety and Shared Resources
- Thread Safety and Immutability
- Java Memory Model
- Java Synchronized Blocks
- Java Volatile Keyword
- Java ThreadLocal
- Java Thread Signaling
Java并发典型的问题
Java并发构造解决上面问题
Locks in Java Read / Write Locks in Java Reentrance Lockout Semaphores Blocking Queues Thread Pools Compare and Swap
进一步学习
Anatomy of a Synchronizer Non-blocking Algorithms Amdahl’s Law References
多线程的优点
- 更好的资源利用
- 在某些情况下更简单的设计
- 更快的程序
多线程的成本
- 更复杂的设计
多线程的某些部分比单线程更加简单,但是也有部分更加复杂,多线程访问共享变量要特别注意,错误的线程同步引起的错误可能很难重现、检测和修复。
- 上下文切换开销
当cpu从一个可执行线程切换到另一个可执行现场是,cpu需要保存本地的数据,程序的指针等。
并发模型
并发系统能够通过不同的并发模型进行实现,并发模型指定一个线程如何和其他线程进行协作以完成指定的任务,不同的并发模型分割任务的方式不同,并且线程可以以不同方式进行通信和协作。
并发模型和分布式系统的相似性
在并发系统中,不同的线程彼此通信,在分布式系统中,不同的进程批次通信,线程和进程本质上非常相似。 因为并发模型类似于分布式系统体系结构,因此他们通常可以相互借鉴,例如:用工作线程之间分配工作的模型就类似于分布式系统中负载均衡模型。错误处理技术也是如此,日志记录、故障转移、幂等性。
并行工作
在并行工作着并发模型中,委托者将传入的job
分发给不同的worker
,这些worker
并行运行,在不同的线程中,并且可能在不同的cpu
上运行。
- 并行工作的并发模型的优点
并发工作模型的优点是容易理解,如果要增加程序的并行化,只需要添加worker
即可。
- 并行工作的并发模型的缺点
并发模型中有一些不可见的问题,容易引发程序bug
.下面将讲诉具体的问题。
1、共享状态会变得很复杂 在并发模型中,访问共享的数据会使程序变得复杂,比如访问共享的内存数据或者数据库数据等。访问共享状态数据是就容易出现线程之间的竞争和死锁。当访问共享数据时多线程之间就回出现阻塞,部分并行化就会丢失,虽然有无阻塞并发算法,但是实现比较难。还可以通过使用持久数据结构来解决这一问题。
流水线模型
如下显示的是流水线模型的流程图
功能并行化
功能并行化的难点在于知道那个函数功能需要调用并行化,跨cpu的调用是需要付出很大的开销的,需要完成的功能要具有一定的复杂度和时间消耗,如果需要完成的功能很小,那么并行化的结果可能比单cpu
更慢。在java 8
中,streaming
就是功能并行化的。
创建和启动Java线程
有两种方法可以指定线程应该执行的代码,第一种是继承父类Thread
,并重写run
方法,第二种方法是实现接口Runnable
,并把实现类对象传入到Thread
构造函数中。
- 第一种写法
class MyThread extends Thread{
public void run(){
System.out.println("Hello MyThread");
}
}
Thread thread = new MyThread();
thread.start();
- 第二种写法
class MyRunnable implements Runnable{
public void run(){
System.out.println("MyRunnable running");
}
}
Thread thread = new Thread(new MyRunnable());
thread.start();
指定线程名称
在创建线程是可以指定线程的名称,这样就可以区分去执行的线程。
- 第一种方式
public class MyThread extends Thread {
public MyThread(String threadName) {
super(threadName);
}
@Override
public void run() {
System.out.println("hello world!");
}
}
Thread thread = new MyThread("demo");
thread.start();
System.out.println(thread.getName());
- 第二种方式
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("MyRunnable is run.");
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable(),"demo");
thread.start();
System.out.println(thread.getName());
}
}
暂停线程
Thread.sleep(10*1000L); //休眠毫秒数,10s
停止线程
停止Java 线程需要自己实现一些代码,不推荐使用原生的stop()
方法,这个方法是被弃用的,这个方法不能保证线程中使用的所以对象的状态,如果应用程序中的其他线程也可以访问这个对象,那么应用程序可能意外的就失败了。
通过自己实现一个doStop()
方法,来让线程推出,这样做的结果就是进入到run
方法之后的代码需要都执行完毕才能推出。
- 第一种实现
public static class MyThread extends Thread{
private boolean keepRun = true;
public MyThread(String threadName){
super(threadName);
}
public synchronized void doStop(){
this.keepRun = false;
}
@Override
public void run() {
while (keepRun){
try {
Thread.sleep(5 * 1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("MyRunnable is run.");
System.out.println("name:" + Thread.currentThread().getName());
}
}
}
MyThread thread = new MyThread("demo");
thread.start();
System.out.println(thread.getName());
Thread.sleep(5*1000L);
thread.doStop();
- 第二种实现
class MyRunnable implements Runnable{
private boolean keepRun = true;
public synchronized void doStop() {
this.keepRun = false;
}
@Override
public void run() {
while (keepRun) {
try {
Thread.sleep(1 * 1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("MyRunnable is run.");
System.out.println("name:" + Thread.currentThread().getName());
}
}
}
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable, "demo");
thread.start();
System.out.println(thread.getName());
Thread.sleep(5*1000L);
myRunnable.doStop();
线程安全和共享资源
多个线程同时调用安全的代码称为线程安全,如果一段代码是线程安全的,那么它不包含竞争条件,只有多个线程对同一个共享资源进行写操作时才会出现竞争条件,因此,了解Java线程在执行是哪些共享的资源非常重要。
- 局部变量
局部变量是存储在每个线程的堆栈中,这意味这局部变量是不共享资源的,是线程安全的。如下的value
变量就是局部变量。
public void add(){
int value = 0;
value ++;
}
- 本地对象引用
本地对象引用本身不共享,但是,引用的对象不存储在每个线程的本地堆栈中,而是存储在共享的堆栈中的。MySum
就是本地对象引用。只要不把这个引用传递给其他线程,这个变量就是线程安全的。
public class MyCount {
public void add(int value){
MySum sum = new MySum();
sum.sum(value);
}
}
MyCount count = new MyCount();
new Thread(new MyRunnable(count, 2)).start();
new Thread(new MyRunnable(count, 3)).start();
- 对象成员变量
对象成员变量是与对象一起存储在堆信息上的,如果多个线程同时对同一个对象成员变量进行写操作,则该方法不是线程安全的,如下列子中两个线程都对同一个变量进行了操作。
public class NotThreadSafe {
StringBuilder builder = new StringBuilder();
public add(String text){
this.builder.append(text);
}
}
// 不安全的操作
NotThreadSafe sharedInstance = new NotThreadSafe();
new Thread(new MyRunnable(sharedInstance)).start();
new Thread(new MyRunnable(sharedInstance)).start();
public class MyRunnable implements Runnable{
NotThreadSafe instance = null;
public MyRunnable(NotThreadSafe instance){
this.instance = instance;
}
public void run(){
this.instance.add("some text");
}
}
下面是安全的操作,每个线程都操作自己的变量。
public class NotThreadSafe {
StringBuilder builder = new StringBuilder();
public add(String text){
this.builder.append(text);
}
}
new Thread(new MyRunnable(new NotThreadSafe()).start();
new Thread(new MyRunnable(new NotThreadSafe())).start();
- 线程控制规则
资源可以是任何共享资源,如对象、数组、文件、数据库、套接字等。在java
中并不能真正的控制资源,只能得到资源的引用。有时候即使对象的引用是安全的,如果该对象指向了共享资源,那么会导致真个应用处于线程不安全的情况下,比如线程1和线程2各自创建数据库连接,然后两个线程都做同一件事,先判断一个记录是否存在,如果不存在就插入,就回出现线程不安全的情况,插入了两条记录。对于在文件或其它共享资源上运行的线程也可能发生这种情况,因此,区分线程控制的对象是资源还是仅仅是引用资源是很有必要的。
线程安全性和不变性
当多个线程对同一个资源进行访问,并且有多个线程进行写入时,就会出现竞争条件。可以通过让任何线程都不能更改共享资源来做到线程安全,不能更改资源的方法就是不提供Set
方法,如下的就是一个线程安全的资源,一旦实例被创建出来就不能在更改,如果需要操作该实例,那就返回一个新实例。
public class MySum{
private int sum=0;
public MySum(int sum){
this.sum = sum;
}
public int getSum(){
return this.sum;
}
//操作了就返回新实例
public MySum sum(int value){
return new MySum(this.sum+value);
}
}
引用是线程不安全的
即使对象是不可变的,并非因此线程就是安全的,因为对象的引用也可能不是线程安全的。
Java 内存模型
Java内存模型
JVM
把操作系统到内存划分为自己的一个线程堆栈存储区和统一堆栈存储区,下图说明了这个逻辑,在Java虚拟机中运行的每个线程都有自己的线程堆栈,每个线程堆栈都是一个方法栈,会随着代码的执行栈随着变化。
线程栈的信息任何其他线程都是不能访问的,每个线程都有自己线程变量信息,线程中是以方法栈来存储的,每个方法栈存储的都是自己方法栈的信息,存储在方法栈中的资源有:基本类型(boolean,byte,short,char,int,long, float,double)的变量是完全存储在线程栈上的以及每个方法栈中的局部变量的引用和线程变量这三种数据是存储在线程栈中的。
堆包含了整个Java应用程序中创建的所有对象,无论创建该对象的线程是什么,线程栈中保存的都是对象的引用,包括原始类型(Byte,Integer,Long)等,无论是创建该对象的局部变量,还是创建成员变量,该对象都是存在堆上的。 下图说明了存储在线程堆栈上的调用栈和局部变量,以及存储在堆上面的对象。
线程栈中的方法随着调用的代码变化而变化,线程栈中的每个一个元素都是方法栈信息,方法栈中有局部变量以及其他变量的引用,局部变量也可以是基本类型,它存储在线程堆栈中,局部变量也可以是引用变量,也是存储在线程堆栈中的,引用对象本身存储在线程堆上,
对象的成员变量是和对象本身一起存储在堆上,不论是基本类型变量还是引用类型变量,都是存储在堆上面的,静态变量也是和类对象一起存储在堆上的。
所有拥有对象引用的线程都可以访问堆上的对象,当一个线程可以访问一个对象时也可以访问一个对象的公开成员变量。如果两个线程同时访问同一个对象的同一个的方法,它们都可以访问这个对象的局部变量,但是每个线程都有自己的局部变量副本。
下面的调用关系图说明了如上的一个内存模型。两个线程都有一个基本类型的局部变量Local variable1
,还有一个引用类型的局部变量Local variable2
,局部变量v2
两个线程指向的是同一个对象,对象中又有两个引用的成员变量。
public class MyRunnable implements Runnable() {
public void run() {
methodOne();
}
public void methodOne() {
//基本类型的局部变量是存储在线程栈中的
int localVariable1 = 45;
MySharedObject localVariable2 =
MySharedObject.sharedInstance;
//... do more with local variables.
methodTwo();
}
public void methodTwo() {
Integer localVariable1 = new Integer(99);
//... do more with local variable.
}
}
public class MySharedObject {
//static variable pointing to instance of MySharedObject
public static final MySharedObject sharedInstance =
new MySharedObject();
//member variables pointing to two objects on the heap
public Integer object2 = new Integer(22);
public Integer object4 = new Integer(44);
//基本类型的成员变量也是存储在堆上的
public long member1 = 12345;
public long member1 = 67890;
}
如上的代码的内存模型就是和上面讲的一样,线程栈中依次是线程、methodOne
、methodTwo
三个方法栈。localVariable2
通过读取静态不可变的引用赋值,因此任何一个线程取到到的对象都是同一个。
硬件内存模型
现代计算机通常有多个CPU
,再具有多个cpu的现代计算机上,可以同时运行多个线程,每个cpu都能够在任何给定时间运行一个线程,因此,每个cpu的一个线程可能同时在Java应用程序中运行,真正的并行运行。
每个cpu包含一组寄存器,cpu可以在这些寄存器上执行的操作比在主存储器中对变量执行的额操作快很多。每个cpu还具有多级的高速缓存存储器,cpu可以比访问主存储区快的多的速度去访问缓存,但是比访问寄存器慢。
计算机还包含主存储区(RAM),所有cpu都可以访问主存储区,并且主存储区通常比缓存大的多。
通常,当cpu执行某个操作指令时,它会将主存储区中的数据读取到cpu缓存中,把要执行的部分读入到寄存器中,然后对其进行执行操作,当执行完后,它会将内部寄存器刷新到高速缓冲存储区,并在某些时候将值刷新回主存储区。
cpu中的缓存是每次读取内存中的一个存储块,然后一定时间刷新写回到这个存储块中,并不是每次更新时读和写整个缓存。缓存中的数据都是内存中数据的拷贝。
Java内存模型和硬件内存之间的差距
如前面描述的一样,硬件内存架构和Java内存架构是不同的,硬件内存架构不区分线程堆栈和堆,在硬件上,线程堆栈和堆都位于主存储器中,线程堆栈和堆有时可能会存在于CPU
的高速缓存和内存CPU
寄存器中。
当对象和变量可以存储在计算机的各种不同存储区域时,可能会出现某些问题,两个主要问题是:
- 写共享变量的值时出现线程的可见行问题。
- 读、写共享变量的时候会触发竞争条件。
共享对象的可见性问题
当多个线程共享一个对象时,如果没有使用volatile
关键字来声明变量或者没有使用同步代码块,就会出现一个线程对共享变量的更新可能对其他线程是不可见的。
比如:最初一个对象的成员变量是存储在主存储器中。然后,在cpu上运行的线程将对象的成员变量拷贝到cpu缓存中,并进行了成员变量的更新,只要cpu缓存未刷新回主内存,成员变量的更改对于其他线程是不可见的,每个线程最终都可以拥有自己的成员变量副本,每个副本都位于不同的cpu缓存中。
下图说明了两个线程对同一个成员变量进行操作时的情况,线程一和线程二分别拷贝了一份数据到cpu缓存中,并且线程一对数据进行了更新,可是并没有写回到主内存中。
要解决这个问题,可以使用Java的volatile这个关键字来申明这个变量,这个关键字会确保一个给定的变量从主内存中读取和更新的时候总是写回到主内存中。
竞争条件
当多个线程共享一个对象的成员变量时,并且多个线程对这个成员变量进行更新时,可能会发生竞争条件。 比如:如果线程A将count变量读入其cpu缓存中,线程B做同样的事情,但进入不同的cpu缓存,然后,线程A将变量做加一操作,线程B也做同样的操作,现在,变量要被写回到主存储器,但是,两个线程没有进行同步,写回到主存储区中的值只有任意一个线程进行过的累加值结果。
要解决这种竞争问题,需要使用Java synchronized 块,同步块保证在任何给定时间只有一个线程可以进入代码的给定关键部分,同步块还保证在同步块内访问的所有变量都将从主内存读入,当线程退出同步块时,无论变量是否被声明为volatile,所以变量都会被再次写入到主内存中。
Java 同步快
同一对象上的同步代码块只能同时在其中执行一个线程,尝试进入同步块的所有其他线程将被阻塞,直到同步块内的线程退出同步块。
该synchronized
关键字可用于标记四种不同类型的块:
- 实例方法
- 静态方法
- 实例方法中的代码块
- 静态方法中的代码块
每一种同步块的作用对象是不一样的,需要根据自己的需求来进行决定使用那种同步块。
synchronized的实例方法
public class Count(){
private int count;
public synchronized void add(int value){
this.count += value;
}
}
每个实例都是同步执行的,如果多个线程拥有同一个对象引用,那么每个线程就回同步执行这段代码。如果多个线程拥有的引用都不一样,那么就不会同步执行。
Synchronized 静态方法
public class Count(){
private int count;
public static synchronized void add(int value){
this.count += value;
}
}
因为静态方法是类方法,一个jvm中只会有一个静态方法,所有给这个静态方法加同步锁就意味着同时只能有一个线程进行访问。
Synchronized 实例中的方法块同步
public class Count(){
private int count;
public void add(int value){
synchronized(this){
this.count += value;
}
}
}
这种写法和第一种的同步代码块的方式是一样的,都是对同一个对象的引用被多线程调用时需要进行同步执行,如果是每个线程中都是创建自己的对象引用,那么就不会出现同步执行。
Synchronized 静态方法中的代码块
public class MyClass {
public static synchronized void log1(String msg1, String msg2){
log.writeln(msg1);
log.writeln(msg2);
}
public static void log2(String msg1, String msg2){
synchronized(MyClass.class){
log.writeln(msg1);
log.writeln(msg2);
}
}
}
如果使用静态方法的同步代码块,就可以多个线程同时调用这个方法,只是调用进入方法之后同步执行,和同步静态方法的不同点是阻塞的地方不一样。
Java Volatile 关键字
Volatile
这个关键字上面已经讲过了,每次读取一个此关键字声明的变量都会从主存储区中读取,不会从cpu缓存中读取,写入也是每次都会写入到主存储区中,不会只写入到cpu缓存中。
Volatile
的可见性超出了变量本身,可见行会影响如下:
- 如果线程A写入
volatile
变量,那么在写入时线程A所见的变量都会被写入到主存储中。 - 如果线程A读入
volatile
变量,那么在读入时,线程A可见的变量也都将从主存储区中读入。
下面用代码说明这两个可见行:
public class MyClass {
private int years;
private int months
private volatile int days;
public void update(int years, int months, int days){
this.years = years;
this.months = months;
this.days = days;
}
}
这个类中有三个变量,只有一个变量是volatile
,在线程A执行update
方法时,会把三个变量都写入到主存储区中,线程B在进行读取时就回对三个变量可见。
public class MyClass {
private int years;
private int months
private volatile int days;
public int totalDays() {
int total = this.days;
total += months * 30;
total += years * 365;
return total;
}
public void update(int years, int months, int days){
this.years = years;
this.months = months;
this.days = days;
}
}
线程A调用totalDays
方法时,因为days
变量是volatile
的,所以三个变量都将从主内存中读取。
volatile
变量在写的时候要是最后一个变量,读的时候第一个变量,这样才能让方法中的变量可见行进行改变。
指令重排的问题
只要指令的语义不变,JVM和cpu就可以处于性能原因重新排序程序中的指令。 如:
int a = 1;
int b = 1;
a ++;
b ++;
上面的语句会被重排
int a = 1;
a++;
int b = 1;
b++;
但是如果某个变量是volatile
的时候,重排可能就会影响到代码的语义,下面两个中如果update1
方法被指令重排为update2
方法,那它的语义就改变了,update1
方法在更新时volatile
变量是最后更新的,因此会让其他的变量都写入到主存储区中,update2
方法经过指令重排之后,volatile
变量就会第一被写入到主存储区中,另外两个变量会出现不再写入到主存储区中,就会出现可见行不一致问题。
这样的指令重排是有问题的,需要进行处理。Java有一个解决这个问题的方法。
public class MyClass {
private int years;
private int months
private volatile int days;
public void update1(int years, int months, int days){
this.years = years;
this.months = months;
this.days = days;
}
public void update2(int years, int months, int days){
this.days = days;
this.months = months;
this.years = years;
}
}
volatile 不能解决并发行问题
如果对应两个线程对同一个volatile
进行读和写的时候是会出现并发问题的。volatile
是用于解决一个线程对一个变量进行读和写,而其他线程只读取变量,能保证其他线程读取到的变量都是最新的,如果变量不是volatile
这个类型的,就不能保证读取到的是最最新的值。
volatile的性能问题
在使用volatile
变量之后会导致变量读和写都到主存储区,从主存储区读取和写入比从cpu缓存区读取和写入更加耗时,并且,访问volatile
变量会阻止指令重新排序。因此,只有在真正需要对变量的可见性时才应该使用volatile
变量。
Java ThreadLocal
线程变量可以用于线程过程中传递值,即使只有一个引用,线程变量也不会出现并发问题。
private ThreadLocal myThreadLocal = new ThreadLocal();
myThreadLocal.set("A thread local value");
String threadLocalValue = (String) myThreadLocal.get();
Thread Signaling
线程信号的目的是使线程发送一个信号到其他线程,另外,线程信号使线程去等待一个其他线程的信号。比如:一个线程B可能会等待一个信号来自于线程A确定数据已经准备好。
共享信号变量
一种简单的信号共享变量就是通过一个共享对象来设置信号值,线程A可以从同步块内部将布尔变量hasDataToProcess
设置为true
,线程B通过读取共享变量来作为信号,下面是一个简单共享信号变量来做线程信号的例子。
public class MySingle{
protected boolean hasDataToProcess = false;
public synchronized boolean hasDataToProcess(){
return this.hasDataToProcess;
}
public synchronized void setHasDateToProcess(boolean hasDate){
this.hasDataToProcess = hasDate
}
}
线程A和线程B必须都拥有同一个共享变量的引用,这样才能使信号生效,如果两个线程具有不同的引用,就不能使信号变量生效。如下使用:
package com.demo.concurrency;
public class ThreadSingle {
public static void main(String[] args) {
MySingle single = new MySingle();
Thread threadA = new Thread(new MyRunable1(single));
threadA.start();
Thread threadB = new Thread(new MyRunable2(single));
threadB.start();
}
static class MyRunable1 implements Runnable {
private MySingle mySingle;
public MyRunable1(MySingle mySingle) {
this.mySingle = mySingle;
}
@Override
public void run() {
System.out.println("线程A开始发送信号");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.mySingle.setHasDateToProcess(true);
}
}
static class MyRunable2 implements Runnable{
private MySingle mySingle;
public MyRunable2(MySingle mySingle) {
this.mySingle = mySingle;
}
@Override
public void run() {
while (!mySingle.hasDateToProcess()){
System.out.println("等待中");
}
System.out.println("接受到信号");
}
}
static class MySingle {
protected boolean hasDateToProcess = false;
public synchronized boolean hasDateToProcess() {
return this.hasDateToProcess;
}
public synchronized void setHasDateToProcess(boolean hasDate) {
this.hasDateToProcess = hasDate;
}
}
}
wait(),notify(),notifyAll()
线程B在等待是时一只忙等待的,这样是很浪费cpu的,线程应该以某种方式等待或者休眠,直到线程收到在等待的线程。Java中有一个内置的等待机制,可以让线程等待信号是变为非活动状态.
死锁
线程死锁
线程死锁是指一个或多个线程被阻塞等待获取另一个线程中的所持有的锁。死锁会发生在同一个时刻当多个线程需要相同的锁,但是获取锁的顺序不一样时。 比如,两个线程,线程1拥有A的锁,现在要用于B的锁,同时,线程2拥有B的锁,现在获取A的锁,那么这两个线程就会发生死锁。
数据库死锁
数据库事务可能是更容易发生死锁的地方,一个数据库事务包含多个更新sql,当一个记录在事务的更新期间,这一条记录是被锁定的,直到这个事务完成,因此一个数据库事务可能会锁多行记录。 如果多个事务在同一时刻发起请求去更新同一条记录,这样就会发生死锁的情况。 数据库中的锁是很难提取知道的,只有开发应用的人才能避免这种情况。
死锁预防
- 锁订购
- 锁超时
- 死锁检测
线程饥饿和公平
如果一个线程因为其他线程占用了全部的cpu时间而不能获取cpu的使用,这种现象被称为饥饿,这个线程会从饥饿到死亡,因为其他线程完全占用着cpu,线程饥饿的解决办法称为公平。
Java中饥饿产生的原因
- 高优先级的线程从低优先级的线程中占有时间。
解决办法:可以为每个线程设定优先级,优先级越高,cpu占用的时间越长。
- 线程访问同步代码块一只被阻,因为其他线程在这之前一直允许访问。
Java的同步代码块是另一个线程饥饿的原因,同步代码块不能保证允许进入方法的线程的顺序,可能出现一个线程每次访问时都是被阻塞的状态。
- 线程调用了
wait()
方法,一直在等待,因为其他线程一直被唤醒。
如果多个线程调用了同一个对象的wait
方法,则notiy
方法不能保证唤醒那个线程,因此,可能会出现有线程永远没有被唤醒的机会。
Java中使用公平
- 使用锁而不是同步快