快速开始:创建并运行Java线程
Java线程类也是一个object类,它的实例都继承自java.lang.Thread或其子类。创建线程并运行相应代码有两种方式:一种是创建Thread子类的一个实例并重写run方法,第二种是创建类的时候实现Runnable接口。
1,创建Thread的子类
创建Thread子类的一个实例并重写run方法,run方法会在调用start()方法之后被执行。例子如下:
public class MyThread extends Thread {
public void run(){
System.out.println("MyThread running");
}
}
然后创建上述Thread子类的实例,并调用start方法:
MyThread myThread = new MyThread();
myTread.start();
上边的写法比较标准,实际上,创建一个Thread的匿名子类的方式会更方便:
Thread thread = new Thread(){
public void run(){
System.out.println("Thread Running");
}
};
thread.start();
2,实现Runnable接口
第二种编写线程执行代码的方式是新建一个实现了java.lang.Runnable接口的类的实例,实例中的方法可以被线程调用。下面给出例子:
public class MyRunnable implements Runnable {
public void run(){
System.out.println("MyRunnable running");
}
}
为了使线程能够执行run()方法,需要在Thread类的构造函数中传入 MyRunnable的实例对象。示例如下:
Thread thread = new Thread(new MyRunnable());
thread.start();
同样的,也可以创建一个实现了Runnable接口的匿名类,如下所示:
Runnable myRunnable = new Runnable(){
public void run(){
System.out.println("Runnable running");
}
}
Thread thread = new Thread(myRunnable);
thread.start();
线程代码举例:
这里是一个小小的例子。首先输出执行main()方法线程名字。这个线程JVM分配的。然后开启10个线程,命名为1~10。每个线程输出自己的名字后就退出。
public class ThreadExample {
public static void main(String[] args){
//这个输出应该是main,就是main线程的意思
System.out.println(Thread.currentThread().getName());
for(int i=0; i<10; i++){
new Thread("" + i){
public void run(){
//进入的时候暂停1毫秒,增加并发问题出现的几率
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("Thread: " + getName() + "running");
}
}.start();
}
}
}
需要注意的是,尽管启动线程的顺序是有序的,但是执行的顺序并非是有序的,这是因为线程是并行执行而非顺序的,Jvm和操作系统一起决定了线程的执行顺序。
几个线程相关的概念
共享资源的互斥性
我们通常允许多个线程同时对数据进行读操作,但同一时间内只允许一个线程对数据进行写操作。所以我们通常将锁分为共享锁和排它锁,也叫做读锁和写锁。例如,对于不可变的数据共享,所有线程都只能对其进行读操作,所以不用考虑线程安全问题。但是对共享数据的写操作,一般就需要保证互斥性,互斥是指同时只允许一个访问者对其进行访问,具有唯一性和排它性。Java 中提供多种机制来保证互斥性,最简单的方式是使用Synchronized。
竞态条件,临界区
当多个线程同时访问同一个资源,并且其中的一个或者多个线程对这个资源进行了写操作,就会产生竞态条件。导致竞态条件发生的代码区称作临界区。在临界区中使用适当的同步就可以避免竞态条件。
原子性
原子性就是指对数据的操作是一个独立的、不可分割的整体。换句话说就是,一次操作是一个连续不可中断的过程,数据不会执行到一半的时候被其他线程所修改。保证原子性的最简单方式是操作系统指令,就是说如果一次操作对应一条操作系统指令,这样肯定可以能保证原子性。但是很多操作不能通过一条指令就完成。例如,对long类型的运算,很多系统就需要分成多条指令分别对高位和低位进行操作才能完成。还比如,我们经常使用的整数 i++ 的操作,其实需要分成三个步骤:(1)读取整数 i 的值;(2)对 i 进行加一操作;(3)将结果写回内存。所以 i++操作不是线程安全的。对于这种组合操作,要保证原子性,最常见的方式是加锁,如Java中的Synchronized或Lock都可以实现。
可见性
要理解可见性,需要先对JVM的内存模型有一定的了解(后续有较详细介绍),JVM的内存模型与操作系统类似,如图所示:
从这个图中我们可以看出,每个线程都有一个自己独立的内存空间(相当于CPU高级缓冲区,这么做的目的还是在于进一步缩小存储系统与CPU之间速度的差异,提高性能),对于共享变量,线程每次读取的是工作内存中共享变量的副本,写入的时候也直接修改工作内存中副本的值,然后在某个时间点上再将工作内存与主内存中的值进行同步。这样导致的问题是,如果线程1对某个变量进行了修改,线程2却有可能看不到线程1对共享变量所做的修改。Java 中可通过Synchronized或Volatile来保证可见性。被Synchronized修饰的代码可以保证在同一时刻仅有一个线程可以进入代码的临界区。还可以保证代码块中所有被访问的变量将会从主存中读入,当线程退出同步代码块时,所有被更新的变量都会被刷新回主存中去,不管这个变量是否被声明为volatile。
JVM内存模型
Java虚拟机是一个完整的计算机的一个模型,因此这个模型自然也包含一个内存模型,并尝试屏蔽掉各种硬件和操作系统的访问差异。JVM内存模型的目标:定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存取出来这样的细节。
Java内存模型把Java虚拟机内部划分为线程栈和堆。每一个运行在Java虚拟机里的线程都拥有自己独立的线程栈。这个线程栈包含了这个线程调用的方法当前执行点相关的信息。一个线程仅能访问自己的线程栈。一个线程创建的本地变量对其它线程不可见,仅自己可见。即使两个线程执行同样的代码,这两个线程仍然在自己的线程栈中创建本地变量。因此,每个线程拥有每个本地变量的独有版本。
所有原始类型的本地变量都存放在线程栈上,因此对其它线程不可见。一个线程可能向另一个线程传递一个原始类型变量的拷贝,但是它不能共享这个原始类型变量自身。堆上包含在Java程序中创建的所有对象,无论是哪一个对象创建的。这包括原始类型的对象版本。如果一个对象被创建然后赋值给一个局部变量,或者用来作为另一个对象的成员变量,这个对象仍然是存放在堆上。如图:
注意以下几种情况:
(1)一个本地变量可能是原始类型,在这种情况下,它总是“呆在”线程栈上。
(2)一个本地变量也可能是指向一个对象的一个引用。在这种情况下,引用(这个本地变量)存放在线程栈上,但是对象本身存放在堆上。
(3)一个对象可能包含方法,这些方法可能包含本地变量。这些本地变量仍然存放在线程栈上,即使这些方法所属的对象存放在堆上。
(4)一个对象的成员变量可能随着这个对象自身存放在堆上。不管这个成员变量是原始类型还是引用类型。
(5)静态成员变量跟随着类定义一起也存放在堆上。
(6)存放在堆上的对象可以被所有持有对这个对象引用的线程访问。当一个线程可以访问一个对象时,它也可以访问这个对象的成员变量。如果两个线程同时调用同一个对象上的同一个方法,它们将会都访问这个对象的成员变量,但是每一个线程都拥有这个变量的私有拷贝(参考上一小节“可见性”)。
硬件内存架构
现代硬件内存模型与Java内存模型有一些不同。理解内存模型架构以及Java内存模型如何与它协同工作也是非常重要的。下面是现代计算机硬件架构的简单图示:
一个现代计算机通常由两个或者多个CPU。每个CPU都包含一系列的寄存器,它们是CPU内存的基础。CPU在寄存器上执行操作的速度远大于在主存上执行的速度,这是因为CPU访问寄存器的速度远大于主存。每个CPU还有一个CPU 缓存层,CPU访问缓存层的速度快于访问主存的速度但慢于访问内部寄存器的速度。一个计算机还包含一个主存,所有的CPU都可以访问主存,主存通常比CPU中的缓存大得多。
通常情况下,当一个CPU需要读取主存时,它会将主存的部分读到CPU缓存中。它甚至可能将缓存中的部分内容读到它的内部寄存器中,然后在寄存器中执行操作。当CPU需要将结果写回到主存中去时,它会将内部寄存器的值刷新到缓存中,然后在某个时间点将值刷新回主存。这就解释了“可见性”问题,一个线程读主存的数据并修改,在它刷新回主存之前,这个过程对别的线程是不可见的,别的线程从主存中读到的数据还是未修改的。
Java线程有几种状态?
Java线程在某个时刻只能处于以下六个状态中的一个。
- New(新创建),一个线程刚刚被创建出来,还没有开始运行的状态,更通俗点说:还没有调用start方法;
- Runnable(可运行),可以在Java虚拟机中运行的状态,一个可运行的线程可能正在运行自己的代码也可能没有,这取决于操作系统提供的时间片;
- Blocked(被阻塞),当一个线程试图获取一个内部的对象锁(不是java.util.concurrent库中的锁),而该锁此时正被其他线程持有,则该线程进入阻塞状态;
- Waiting(等待),当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。在调用Object.wait方法或Thread.join方法,或者是等待java.util.concurrent库中的Lock或Condition时,就会出现这种情况;
- Timed waiting(计时等待),Object.wait、Thread.join、Lock.tryLock和Condition.await等方法有超时参数,还有Thread.sleep方法、LockSupport.parkNanos方法和LockSupport.parkUntil方法,这些方法会导致线程进入计时等待状态,如果超时或者出现通知,都会切换回可运行状态;
- Terminated(被终止),因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。
几种状态的转化关系,如下图所示:
死锁
如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。
产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
上述是进程死锁描述,线程死锁类似,那么如何避免死锁呢?
(1)加锁顺序,确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生,即优先级。
(2)加锁时限,在尝试获取锁的时候加一个超时时间,若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁。
(3)死锁检测,是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。
那么当检测出死锁时,这些线程该做些什么呢?
一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁。
一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。
原文来自:并发编程网-Java并发性和多线程介绍目录
liuxiaopeng-Java 并发编程:核心理论
并发编程网-Java面试题-基础知识
并发编程网-避免死锁