转载请保留原文链接: http://dashidan.com/article/java/basic/19.html
部分内容源自:《深入理解JVM虚拟机》.
① 线程和进程的概念
进程是指一个内存中运行有自己独立的一块内存空间的应用程序.一个进程中至少一个线程.
线程是指进程中的一个执行流程.每个线程有独立的运行栈. 线程从属于进程. 一个进程中可以运行多个线程.进程中的多个线程共享进程的内存.
线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止.
多进程是指操作系统能同时运行多个任务(程序).
多线程是指在同一程序中,有多个顺序流在执行.
② 线程的实现
线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度(线程是CPU调度的基本单位).
主流的操作系统都提供了线程实现,Java语言则提供了在不同硬件和操作系统平台下对线程操作的统一处理.
Thread类中所有关键方法都是声明为Native的,在Java API中,本地方法往往意味着这个方法没有使用或无法使用平台无关的手段来实现(当然也有可能是为了执行效率而使用Native方法)
③ Java线程调度
Java的线程调度方式是抢占式调度,虽然Java线程的调度是系统自动完成的,但是我们还是可以“建议”系统给某些线程多分配一点执行时间,另外的一些线程则可以少分配一点——这项操作可以通过设置优先级来完成.
不过,线程的优先级并不是太靠谱,因为Java线程是通过映射到原生线程上来实现的,所以线程调度最终还是取决于操作系统,虽然现在很多操作系统都提供了优先级的概念,但是并不见得与Java线程的优先级一一对应.比如:Windows中就只有7种线程优先级,而Java语言一共设置了10个级别的线程优先级.
④ Java中线程的实现 — 在Java中想实现多线程有两种手段,一种是集成Thread类,另一种就是实现Runnable接口. ###1.继承自Thread类package com.dashidan.lesson18;
/**
* 大屎蛋教程网-dashidan.com
* <p>
* Java教程基础篇: 18.Java线程
* 继承自Thread类
*/
public class MyThread1 extends Thread {
@Override
public void run() {
System.out.println("MyThread1 run.");
}
}
###2.实现Runnable接口 首先定义一个线程类继承自Runnable接口,如:
package com.dashidan.lesson18;
/**
* 大屎蛋教程网-dashidan.com
* <p>
* Java教程基础篇: 18.Java线程
* 实现Runnable接口
*/
public class MyThread2 implements Runnable {
@Override
public void run() {
System.out.println("MyThread2 run.");
}
}
入口类,实例化线程类的对象,发动启动线程的命令
package com.dashidan.lesson18;
/**
* 大屎蛋教程网-dashidan.com
* <p>
* Java教程基础篇: 18.Java线程
*/
public class Demo1 {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
/** t1 线程启动*/
t1.start();
MyThread2 myThread2 = new MyThread2();
Thread t2 = new Thread(myThread2);
/** t2 线程启动*/
t2.start();
}
}
输出:
MyThread2 run.
MyThread1 run.
输出的顺序可能不一致
多运行几次, 输出的顺序可能不一致, 这个是由于线程的执行顺序无法保证导致.
继承Thread和实现Runnable接口如何选择
由于Java是单根继承体系, 当一个类需要继承与其他类, 还需要具有线程的能力, 这时只能采用实现Runable接口的方式.
⑤ 启动线程
在线程的Thread对象上调用start()方法,而不是run()或者别的方法.在调用start()方法之前,线程处于新状态中, 新状态指有一个Thread对象, 但还没有一个真正的线程. 在调用start()方法之后,发生了一系列复杂的事情
- 启动新的执行线程(具有新的调用栈).
- 该线程从新状态转移到可运行状态.
- 当该线程获得机会执行时,其目标run()方法将运行.
run()方法
对Java来说,run()方法是线程启动的入口方法, 同时也是一个普通的方法. 因此,在Runnable上或者Thread上调用run方法是合法的.但并不启动新的线程.所以不建议直接调用run()方法.
⑥ Java线程的状态
Java线程具有五中基本状态
- 新建状态(New)
在生成线程对象,并没有调用该对象的start方法.如:
Thread t = new MyThread1();
- 就绪状态(Runnable)
当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态.随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行. 在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态.
- 运行状态(Running)
线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码.就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中.
- 阻塞状态(Blocked)
线程正在运行的时候,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态.通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行.sleep,suspend,wait等方法都可以导致线程阻塞.
处于运行状态中的线程直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态.根据阻塞产生的原因不同,阻塞状态又可以分为三种:
- 等待阻塞
运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态.wait()、notify()、notifyAll()是三个定义在Object类里的方法,可以用来控制线程的状态.任何一个时刻,对象的控制权(monitor)只能被一个线程拥有.
无论是执行对象的wait、notify还是notifyAll方法,必须保证当前运行的线程取得了该对象的控制权(monitor)
如果在没有控制权的线程里执行对象的以上三种方法,就会报java.lang.IllegalMonitorStateException异常.
执行时wait(), sleep()时会抛出InterruptedException, 需要放在try-catch语句中执行.
报错示例:
package com.dashidan.lesson18;
/**
* 大屎蛋教程网-dashidan.com
* <p>
* Java教程基础篇: 18.Java线程
* 线程阻塞状态-wait()
*/
public class WaitThread extends Thread {
@Override
public void run() {
System.out.println("WaitThread run.");
try {
System.out.println("WaitThread before wait.");
this.wait();
System.out.println("WaitThread after wait.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
入口类:
package com.dashidan.lesson18;
/**
* 大屎蛋教程网-dashidan.com
* <p>
* Java教程基础篇: 18.Java线程
* 线程阻塞状态 wait()
*/
public class Demo2 {
public static void main(String[] args) {
WaitThread waitThread = new WaitThread();
waitThread.start();
}
}
输出:
WaitThread run.
WaitThread before wait.
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at com.dashidan.lesson18.WaitThread.run(WaitThread.java:15)
线程取得控制权的方法有三种:
- 执行对象的某个同步实例方法.
- 执行对象对应类的同步静态方法.
- 执行对该对象加同步锁的同步块.
如果对象调用了wait方法就会使持有该对象的线程把该对象的控制权交出去,然后处于等待状态.如果对象调用了notify方法就会通知某个正在等待这个对象的控制权的线程可以继续运行, 由Java虚拟机来决定哪个线程继续运行.
如果对象调用了notifyAll方法就会通知所有等待这个对象控制权的线程继续运行可以通过synchronize关键字来获取同步锁, 然后再调用wait(), notify(), notifyAll().
package com.dashidan.lesson18;
/**
* 大屎蛋教程网-dashidan.com
* <p>
* Java教程基础篇: 18.Java线程
* 线程阻塞状态 wait() , 获得同步锁
*/
public class Demo3 {
public static Object object = new Object();
public static void main(String[] args) {
WaitThread waitThread = new WaitThread(object);
waitThread.start();
try {
/** 主线程休眠3秒*/
System.out.println("主线程休眠3秒");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
/** 3秒后执行 notifyAll */
synchronized (object) {
object.notifyAll();
}
}
}
class WaitThread extends Thread {
Object object;
public WaitThread(Object object) {
this.object = object;
}
@Override
public void run() {
System.out.println("WaitThread run.");
synchronized (object) {
try {
System.out.println("WaitThread before lock " + this.getName());
object.wait();
System.out.println("WaitThread after lock " + this.getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
输出:
主线程休眠3秒
WaitThread run.
WaitThread before lock Thread-0
WaitThread after lock Thread-0
- 同步阻塞
线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态. 当锁被释放后, 其他获得该锁的线程继续执行.
package com.dashidan.lesson18;
/**
* 大屎蛋教程网-dashidan.com
* <p>
* Java教程基础篇: 18.Java线程
* 线程阻塞状态 同步阻塞
*/
public class Demo5 {
public static Object lock = new Object();
public static void main(String[] args) {
SyncThread syncThread = new SyncThread("t1", lock);
syncThread.start();
SyncThread syncThread1 = new SyncThread("t2",lock);
syncThread1.start();
}
}
class SyncThread extends Thread {
public Object lock;
public SyncThread(String name, Object lock) {
super(name);
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
/** 一个线程执行这个语句块的时候,另一个线程等待. */
System.out.println(this.getName() + " before sleep.");
Thread.sleep(3000);
System.out.println(this.getName() + " after sleep.");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出:
t2 before sleep.
t2 after sleep.
t1 before sleep.
t1 after sleep.
多运行几次, 输出结果可能不一致, 但每次都是执行完一个线程, 再执行另一个.
sleep()方法会释放cpu, 进入等待执行的状态, 但不会释放所持对象的锁.
- 其他阻塞
通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态.当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态.
sleep示例:
package com.dashidan.lesson18;
/**
* 大屎蛋教程网-dashidan.com
* <p>
* Java教程基础篇: 18.Java线程
* 线程阻塞状态 sleep()
*/
public class Demo4 {
public static void main(String[] args) {
SleepThread thread = new SleepThread();
thread.start();
}
}
class SleepThread extends Thread {
@Override
public void run() {
try {
System.out.println("before sleep.");
/** 休眠3秒*/
Thread.sleep(3000);
/** 3秒后输出*/
System.out.println("after sleep.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出:
before sleep.
after sleep.
- 死亡状态(Dead)
如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡结束生命周期.对于已经死亡的线程,无法再使用start方法令其进入就绪.
⑦ 线程死锁
线程死锁是指2个线程都在同步阻塞状态, 等待对象的锁, 在对方的线程中无法释放. 这样会导致这两个线程卡死, 无法继续执行. 开发时应极力避免.
package com.dashidan.lesson18;
/**
* 大屎蛋教程网-dashidan.com
* <p>
* Java教程基础篇: 18.Java线程
* 线程死锁
*/
public class Demo6 {
public static LockObject lockA = new LockObject("LOCK-A");
public static LockObject lockB = new LockObject("LOCK-B");
public static void main(String[] args) {
/** 注意这两个线程传入的参数,顺序不一样, lockA, lockB*/
LockThread thread1 = new LockThread("t1", lockA, lockB);
thread1.start();
/** 注意这两个线程传入的参数,顺序不一样, lockB, lockA*/
LockThread thread2 = new LockThread("t2", lockB, lockA);
thread2.start();
}
}
class LockThread extends Thread {
LockObject lock0;
LockObject lock1;
public LockThread(String name, LockObject lock0, LockObject lock1) {
super(name);
this.lock0 = lock0;
this.lock1 = lock1;
}
@Override
public void run() {
try {
synchronized (lock0) {
System.out.println(getName() + "持有对象锁 " + lock0.getName());
Thread.sleep(3000);
System.out.println(getName() + "等待对象锁 " + lock1.getName() + " ... ");
synchronized (lock1) {
System.out.println(getName() + "持有对象锁 " + lock1.getName());
Thread.sleep(3000);
}
/** Attention!这一行并没有输出*/
System.out.println(getName() + "执行完毕");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 一个简单的锁对象
*/
class LockObject {
/**
* 对象名字
*/
private String name;
public LockObject(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
输出:
t1持有对象锁 LOCK-A
t2持有对象锁 LOCK-B
t2等待对象锁 LOCK-A ...
t1等待对象锁 LOCK-B ...
执行完毕这一句并没有输出.
是因为两个线程进入了线程死锁状态, 互相等待对方释放锁. 编码过程中要极力避免嵌套调用锁的情况出现..
⑧ synchronized用在对象, 方法, 代码块的区别 — synchronized关键字的本质是`锁对象`. 用在对象类型上, 即为锁定目标对象. 用在方法和代码块中, 表示锁定当前对象.对象一旦被锁定, 所有的synchonized关键字修饰的区域都需要等待锁释放后才能执行. 前边几个例子都是synchronized锁对象的方式.下面来看看用锁方法和锁代码块.package com.dashidan.lesson18;
/**
* 大屎蛋教程网-dashidan.com
* <p>
* Java教程基础篇: 18.Java线程
* synchronized锁方法和区块
*/
public class Demo7 {
public static void main(String[] args) {
SyncObject syncObject = new SyncObject();
Demo7Thread t1 = new Demo7Thread("t1", syncObject);
t1.start();
Demo7Thread t2 = new Demo7Thread("t2", syncObject);
t2.start();
}
}
class SyncObject {
/**
* synchronized 锁方法
*/
public synchronized void testSyncMethod(String name) {
try {
System.out.println(name + "运行testSyncMethod开始.");
Thread.sleep(300);
System.out.println(name + "运行testSyncMethod结束.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* synchronized 锁代码区块
*/
public void testSyncCode(String name) {
synchronized (this) {
try {
System.out.println(name + "运行testSyncCode开始.");
Thread.sleep(300);
System.out.println(name + "运行testSyncCode结束.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Demo7Thread extends Thread {
SyncObject syncObject;
public Demo7Thread(String name, SyncObject syncObject) {
super(name);
this.syncObject = syncObject;
}
@Override
public void run() {
syncObject.testSyncMethod(this.getName());
syncObject.testSyncCode(this.getName());
}
}
输出:
t1运行testSyncMethod开始.
t1运行testSyncMethod结束.
t2运行testSyncMethod开始.
t2运行testSyncMethod结束.
t1运行testSyncCode开始.
t1运行testSyncCode结束.
t2运行testSyncCode开始.
t2运行testSyncCode结束.
运行结果可能不一样, 但开始和结束总是一一对应.在开始和结束之间没有执行其他方法, 说明都是锁的同一个对象.可以将任意一个synchronized屏蔽,再跑一下程序,就会发现输出中的开始和结束不再一一对应.
⑨ Lock对象
JDK1.5开始提供了一个更加高效的锁的方式.concurrent包中Lock对象.最大的优势是可以读写锁分离, 加入写锁后, 读锁需要等写锁释放后再进行. 加入读锁后, 还是可以获取写锁, 为了保证数据安全, 使用写锁的时候. 同一个对象可以使用多个Lock对象, 锁定对应的范围, 而不像synchonized锁整个对象.
官方例子
class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// Must release read lock before acquiring write lock
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// Recheck state because another thread might have
// acquired write lock and changed state before we did.
if (!cacheValid) {
data =...
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}
一个简单的读写锁例子
package com.dashidan.lesson18;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 大屎蛋教程网-dashidan.com
* <p>
* Java教程基础篇: 18.Java线程
* Lock对象
*/
public class Demo8 {
public static void main(String[] args) {
LockObjectReadAndWrite lockObj = new LockObjectReadAndWrite();
/** 写线程*/
WriteThread writeThread = new WriteThread("w1", lockObj);
writeThread.start();
/** 读线程*/
ReadThread readThread = new ReadThread("r1", lockObj);
readThread.start();
}
}
class LockObjectReadAndWrite {
private final Map<String, Integer> m = new HashMap<>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock readLock = rwl.readLock();
private final Lock writeLock = rwl.writeLock();
public Integer get(String key, String threadName) {
System.out.println("加入读锁 " + threadName);
readLock.lock();
try {
System.out.println("读取休息1秒");
Thread.sleep(1000);
return m.get(key);
} catch (InterruptedException e) {
e.printStackTrace();
return -1;
} finally {
readLock.unlock();
System.out.println("释放读锁 " + threadName);
}
}
public Integer put(String key, Integer value, String threadName) {
System.out.println("------WriteThread 加入写锁------ : " + threadName);
writeLock.lock();
try {
System.out.println("写入休息6秒.....");
Thread.sleep(6000);
return m.put(key, value);
} catch (InterruptedException e) {
e.printStackTrace();
return -1;
} finally {
writeLock.unlock();
System.out.println("------WriteThread 释放写锁------: " + threadName);
}
}
}
class WriteThread extends Thread {
LockObjectReadAndWrite lockObj;
public WriteThread(String name, LockObjectReadAndWrite lockObj) {
super(name);
this.lockObj = lockObj;
}
@Override
public void run() {
while (true) {
/** 持续写入*/
lockObj.put("a", 1, this.getName());
/** 写入线程休息3秒*/
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class ReadThread extends Thread {
LockObjectReadAndWrite lockObj;
public ReadThread(String name, LockObjectReadAndWrite lockObj) {
super(name);
this.lockObj = lockObj;
}
@Override
public void run() {
while (true) {
/** 持续读取*/
lockObj.get("a", this.getName());
}
}
}
输出:
加入读锁 r1
读取休息1秒
------WriteThread 加入写锁------ : w1
释放读锁 r1
写入休息6秒.....
加入读锁 r1
------WriteThread 释放写锁------: w1
读取休息1秒
释放读锁 r1
加入读锁 r1
读取休息1秒
释放读锁 r1
加入读锁 r1
读取休息1秒
------WriteThread 加入写锁------ : w1
写入休息6秒.....
释放读锁 r1
加入读锁 r1
输出结果可能不一样. 但每次加上写锁之后, 读取的操作便停止. 释放写锁后, 读取继续.
⑩ 线程常见问题
- 线程的名字
默认线程名:Thread-
加下一线程数
:
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
自定义线程名:
在构造函数中传入线程名.
public Thread(Runnable target, String name) {
init(null, target, name, 0);
}
- 获取当前线程对象的方法
Thread.currentThread().
- 当线程目标
run()
方法结束时, 该线程结束. - 一个线程只能启动一次, 一旦线程启动, 不能再重新启动.
- 线程的调度由Java虚拟机(JVM)控制.
在单核CPU的电脑上,实际上一次只能运行一个线程,CPU分时处理,看上去向同步运行一样.多核CPU的电脑同时可以运行多个线程.Java虚拟机决定实际运行哪个线程.有多个可运行线程时,其中的某一个会被Java虚拟机选为当前线程.默认情况下不能保证线程运行的先后顺序.量子理论的上帝掷骰子.