Java多线程(一)
一、进程与线程
1、进程
进程是处于运行状态的程序:当一个程序进入内存运行时,就会变成一个进程。进程是系统分配资源和调度的基本单位。
2、线程
线程是进程的组成部分,也被称为轻量级进程。线程是进程的执行单元,一个进程可以创建多个线程。线程可以拥有自己的堆栈、程序计数器等,但不再拥有系统资源,它与(父)进程的其他线程共享进程的资源。
一个线程可以创建和撤销另一个线程,同一个进程中的多个线程可以并发执行,线程的调度和管理由进程完成。
3、多线程优势
-
与进程相比,线程之间的隔离度小,可以很方便的共享内存、文件句柄和进程的状态等
-
由于创建线程的代价比创建进程的代价小很多,因此使用多线程比多进程效率高
二、线程的创建
1、继承Thread类
- 继承
Thread
类,重写run
方法
public class MyThread extends Thread{
@Override
public void run() {
//Do Something
System.out.println(getName());
}
}
- 创建对象,调用该对象的
start
方法启动线程
new MyThread().start();
注意:使用此种方式创建线程,多条线程之间无法共享线程类的实例变量。
2、实现Runnable接口
- 实现Runnable接口,重写run方法
public class MyThread implements Runnable{
@Override
public void run() {
//Do Something
System.out.println(Thread.currentThread().getName());
}
}
- 创建
Runnable
接口实现类的实例,并将此实例作为Thread
的target来创建Thread
对象,调用Thread
对象的start
方法启动线程
Runnable target = new MyThread();
new Thread(target).start();
new Thread(target, "another thread").start();
使用此种方式创建的多条线程可以共享线程类(target)的实例属性。
public class ThreadShare implements Runnable{
private int count = 0;
@Override
public void run() {
for(int i = 0; i < 5; i++){
System.out.println(count++);
}
}
public static void main(String[] args) {
Runnable share = new ThreadShare();
new Thread(share).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(share).start();
}
}
运行上面的程序,可以看到依次输出了:0~9
三、线程生命周期
线程的生命周期可以大致分为五种状态:新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)、终止(Terminated)。
当使用new
关键字创建一个线程后,该线程就处于新建状态。当调用了线程对象的start()
方法后,该线程就处于就绪状态。如果就绪状态的线程获得了CPU,则该线程处于运行状态。
当线程发生下面的情况时,将会进入阻塞状态:
-
调用
sleep()
方法主动放弃占用的处理器资源 -
在线程中调用了一个阻塞式IO,在返回之前,该线程被阻塞
-
尝试获取一个正被其他线程持有的同步监视器
-
在等待某个通知(notify)
-
使用
suspend()
方法将线程挂起
线程的阻塞状态被解除后会重新进入就绪状态等待再次被调度。当发生下面的情况时,线程的阻塞状态被解除,重新进入就绪状态:
-
已经过了
sleep()
方法的时间 -
调用的阻塞式IO已返回
-
成功获取同步监视器
-
其他线程发出了一个正在等待的通知
-
处于挂起状态的线程被
resume
当线程的run()
方法执行完成、线程抛出一个未捕获的异常时或直接调用了线程的stop()
方法(此方法容易导致死锁,不推荐使用)时,线程将处于终止(死亡)状态。可以使用isAlive()
方法判断线程是否存活。
四、控制线程的执行
1、后台线程
如果一种线程是在后台运行的,并为其他线程提供服务,那么这种线程被称为后台线程或守护线程(Daemon Thread)。可以调用Thread对象的setDaemon(true)
方法将指定的线程设置为后台线程。JVM的垃圾回收线程就是典型的后台线程。
后台线程的特征:如果所有的前台线程死亡,后台线程自动死亡。
public class DaemonThread implements Runnable{
@Override
public void run() {
for(int i = 0; i < 100; i++){
System.out.println(String.format("%s: %s", Thread.currentThread().getName(), i));
}
}
public static void main(String[] args) {
Thread daemonThread = new Thread(new DaemonThread());
daemonThread.setDaemon(true);
daemonThread.start();
for(int i = 0; i < 5; i++){
System.out.println(String.format("%s: %s", Thread.currentThread().getName(), i));
}
}
}
main
线程默认是前台线程;前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。如果要将某个线程设置为后台线程,需要在该线程启动前设置,即setDaemon(true)
在start()
方法前调用。
2、线程优先级
每个线程都具有优先级,默认的优先级与创建它的父线程的优先级相同。main
线程的优先级默认为普通优先级,因此由main
线程创建的子线程也具有普通优先级。
可以使用setPriority(int newPriority)
方法设置线程的优先线,使用getPriority()
方法获取线程的优先级。线程的优先级是一个整数,范围在1~10
之间,Thread类也提供了三个表示优先级的常量:
public final static int MIN_PRIORITY = 1;
//默认优先线
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getPriority());//5
thread.setPriority(7);
System.out.println(thread.getPriority());//7
}
由于不同的操作系统线程的优先线并不相同,并不能很好的和Java的10个优先级对应,因此建议使用Java提供的优先级常量来设置线程优先级,使程序有更好的可移植性。
3、线程睡眠(sleep)
可以使用sleep()
方法将正在执行的线程暂停一段时间,并进入阻塞状态;此线程在sleep
时间内不会获得执行机会,即使没有其他可运行的线程。
public class SleepThread extends Thread{
@Override
public void run() {
System.out.println(new SimpleDateFormat("HH:mm:ss").format(new Date()));
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(new SimpleDateFormat("HH:mm:ss").format(new Date()));
}
public static void main(String[] args) {
new SleepThread().start();
}
}
程序输出:
19:27:19
19:27:21
4、线程加入(join)
可以使用join()
方法让一个线程等待另一个线程完成。当某个线程调用其他线程的join()
方法时,调用此方法的线程将被阻塞,直到使用join()
方法加入的线程执行完成。
public class JoinThread extends Thread{
@Override
public void run() {
for(int i = 1; i < 6; i++){
System.out.println(String.format("%s: %s", getName(), i));
}
}
public static void main(String[] args) {
for(int i = 1; i < 6; i++){
if(i == 3){
JoinThread thread = new JoinThread();
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(String.format("%s: %s", Thread.currentThread().getName(), i));
}
}
}
程序输出:
main: 1
main: 2
Thread-0: 1
Thread-0: 2
Thread-0: 3
Thread-0: 4
Thread-0: 5
main: 3
main: 4
main: 5
5、线程让步(yield)
此方法与sleep()
方法类似也可以使当前线程暂停执行,不同的是调用此方法不会阻塞当前线程,只是将该线程变为就绪状态,使调度器重新调度。实际上,某个线程使用yield()
方法暂停后,只有优先级大于等于当前线程的就绪状态的线程才会获得执行机会。
public static native void yield();
sleep()
方法比yield()
方法有更好的可移植性,通常不建议使用此方法控制并发线程的执行。
五、线程同步
1、线程安全问题
public class ATM {
private Map<String, Integer> account = new HashMap<>();
public void init(){
account.put("BankCard-1", 1000);
}
public void drawMoney(String id, int money){
int balance = account.get(id);
String name = Thread.currentThread().getName();
if(balance >= money){
account.put(id, balance -= money);
System.out.println(String.format("%s从账号%s中取出%s¥!", name, id, money));
}else{
System.out.println(String.format("%s想取钱:%s¥,余额(%s)不足!", name, money, balance));
}
}
public int getBalance(String id){
return account.get(id);
}
}
public class DrawMoneyThread implements Runnable{
private ATM atm;
private String id;
private int money;
public DrawMoneyThread(ATM atm, String id, int money) {
this.atm = atm;
this.id = id;
this.money = money;
}
@Override
public void run() {
atm.drawMoney(id, money);
}
}
public static void main(String[] args){
ATM atm = new ATM();
atm.init();
String id = "BankCard-1";
DrawMoneyThread target = new DrawMoneyThread(atm, id, 600);
new Thread(target, "路人甲").start();
new Thread(target, "路人乙").start();
}
多运行几次可能会看到下面的结果:
路人乙从账号BankCard-1中取出600¥!
路人甲从账号BankCard-1中取出600¥!
上面的例子中,甲和乙同时取钱,ATM中共1000,但他们却分别取出了600,出现了线程安全问题。
2、同步代码块
为了解决线程同步问题,Java引入了同步监视器。使用同步监视器的通用方法就是同步代码块:
synchronized (obj) {
//同步代码块
}
它表示开始执行同步代码块中的代码时,必须先获得对同步监视器(obj)的锁定。任何时该只能有一条线程可以获得对同步监视器的锁定,当同步代码块执行完成后会释放对该同步监视器的锁定。
一般使用可能被并发访问的共享资源做同步监视器。对上面的程序使用同步监视器如下:
public void drawMoney(String id, int money){
synchronized (this) {
int balance = account.get(id);
String name = Thread.currentThread().getName();
if(balance >= money){
account.put(id, balance -= money);
System.out.println(String.format("%s从账号%s中取出%s¥!", name, id, money));
}else{
System.out.println(String.format("%s想取钱:%s¥,余额(%s)不足!", name, money, balance));
}
}
}
3、同步方法
对于多线程的安全问题,Java还提供了同步方法:使用synchronized
关键字修饰的方法被称为同步方法;同步方法无须显式指定同步监视器,同步方法的同步监视器是this
;进入同步方法前同样需要先获得对同步监视器的锁定。修改上面程序中的drawMoney()
方法为同步方法:
public synchronized void drawMoney(String id, int money){
......
}
4、释放对同步监视器的锁定
线程在以下几种情况下会释放对同步监视器的锁定:
-
同步代码块或同步方法执行结束时
-
同步代码块或同步方法中出现了未处理的异常(Error或Exception)时
-
执行同步代码块或同步方法时,程序执行了同步监视器对象的
wait()
方法
在以下几种情况下不会释放对同步监视器的锁定:
-
执行同步代码块或同步方法时,程序调用了
sleep()
或yield()
方法 -
执行同步代码块或同步方法时,其他线程调用了该线程的
suspend()
方法
5、同步锁
Java还提供了一种线程同步机制:通过显式的定义同步锁(Lock)对象来实现同步。
Lock
提供了比同步方法和同步代码块更广泛的锁定操作,它是控制多线程访问共享资源的工具。通常情况下,每次只能有一个线程对Lock对象加锁,线程访问共享资源前应该先获得Lock对象。
使用Lock对象可以显式的加锁和释放锁,对上面的例子使用Lock对象来同步:
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
public void drawMoney(String id, int money){
//加锁
lock.lock();
try{
int balance = account.get(id);
String name = Thread.currentThread().getName();
if(balance >= money){
account.put(id, balance -= money);
System.out.println(String.format("%s从账号%s中取出%s¥!", name, id, money));
}else{
System.out.println(String.format("%s想取钱:%s¥,余额(%s)不足!", name, money, balance));
}
}finally{
//释放锁
lock.unlock();
}
}
6、死锁
当两个线程相互等待对方释放同步监视器时就会发生死锁。
public class DeadLock implements Runnable{
private Object obj = new Object();
private Object another = new Object();
@Override
public void run() {
String name = Thread.currentThread().getName();
if("A".equals(name)){
synchronized (another) {
System.out.println(String.format("%s已获得对 another 对象的锁定!", name));
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(String.format("%s等待获取对 obj 对象的锁定!", name));
synchronized (obj) {
System.out.println(String.format("%s成功锁定 another 和 obj 对象!", name));
}
}
}else{
synchronized (obj) {
System.out.println(String.format("%s已获得对 obj 对象的锁定!", name));
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(String.format("%s等待获取对 another 对象的锁定!", name));
synchronized (another) {
System.out.println(String.format("%s成功锁定 obj 和 another 对象!", name));
}
}
}
}
public static void main(String[] args) {
Runnable target = new DeadLock();
new Thread(target, "A").start();
new Thread(target, "B").start();
}
}
程序输出:
A已获得对 another 对象的锁定!
B已获得对 obj 对象的锁定!
B等待获取对 another 对象的锁定!
A等待获取对 obj 对象的锁定!