一、线程的基本概念
几乎每种操作系统都支持进程的概念 – 进程就是在某种程度上相互隔离的、独立运行的程序
进程 是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。比如在Windows系统中,一个运行的exe就是一个进程。
线程 是指进程中的一个执行流程,一个进程中可以运行多个线程。比如java.exe进程中可以运行很多线程。线程总是属于某个进程,进程中的多个线程共享进程的内存。”同时”执行是人的感觉,在线程之间实际上轮换执行
Java 中的线程通过java.lang.Thread类实现
• JVM 启动时会有一个主方法(main方法)所定义的线程
• 通过创建 Thread类的实例创建新的线程
• 通过 Thread类对象的 run() 方法完成操作,方法run()成为线程体
• 通过调用 Thread类的 start() 方法启动一个线程
二、线程的创建与启动
创建新线程的两种方式:
(1)实现 Runnable 接口
• 定义线程类实现 Runnable接口
• Thread myThread = new Thread(target) //target为 Runnable接口类型
• Runnable中只有一个方法 public void run();
用以定义线程运行体
• 使用 Runnable接口可以为多个线程提供共享的数据
• 实现 Runnable接口的类的 run方法定义中可以使用Thread类的静态方法 Thread.currentThread();
获取当前线程的引用
public class ThreadTest {
static class DoSomething implements Runnable {
private String name;
public DoSomething(String name) {
this.name = name;
}
public void run() {
for (int i = 0; i < 4; i++) {
for (long k = 0; k < 100000000; k++) ;
System.out.print(name + ": " + i+" ");
}
}
}
public static void main(String[] args) {
DoSomething ds1 = new DoSomething("张三");
DoSomething ds2 = new DoSomething("李四");
Thread t1 = new Thread(ds1);
Thread t2 = new Thread(ds2);
t1.start(); // 启动线程
t2.start();
}
}
输出结果:
张三: 0 李四: 0 张三: 1 李四: 1 张三: 2 李四: 2 张三: 3 李四: 3
(2)继承 Thread类
• 定义Thread的子类并重写run方法 class MyThread extends Thread { public void run(){ } }
• 然后生成该类的对象 MyThread myThread = new MyThread();
public class TestThread extends Thread{
public TestThread(String name) {
super(name);
}
public void run() {
for(int i = 0;i<5;i++){
for(long k= 0; k <100000000;k++);
System.out.print(this.getName()+" :"+i);
}
}
public static void main(String[] args) {
Thread t1 = new TestThread("张三");
Thread t2 = new TestThread("李四");
t1.start();
t2.start();
}
}
输出结果:
张三 :0 李四 :0 张三 :1 李四 :1 张三 :2 李四 :2 张三 :3 张三 :4 李四 :3 李四 :4
三、线程的状态
线程的状态转换图(源自:http://lavasoft.blog.51cto.com/62575/27069)
New(新建状态、初始化状态):线程对象已经被创建,但是还没有被启动时的状态。这段时间就是在我们调用new命令之后,调用start()方法之前。
Runnable(可运行状态、就绪状态):在我们调用了线程的start()方法之后线程所处的状态。处于Runnable状态的线程在JAVA虚拟机(JVM)上是运行着的,但是它可能还正在等待操作系统分配给它相应的运行资源以得以运行。
Blocked(阻塞状态、被中断运行):线程正在等待其它的线程释放同步锁,以进入一个同步块或者同步方法继续运行;或者它已经进入了某个同步块或同步方法,在运行的过程中它调用了某个对象继承自java.lang.Object的wait()方法,正在等待重新返回这个同步块或同步方法。
Waiting(等待状态):当前线程调用了java.lang.Object.wait()、java.lang.Thread.join()或者java.util.concurrent.locks.LockSupport.park()三个中的任意一个方法,正在等待另外一个线程执行某个操作。比如一个线程调用了某个对象的wait()方法,正在等待其它线程调用这个对象的notify()或者notifyAll()(这两个方法同样是继承自Object类)方法来唤醒它;或者一个线程调用了另一个线程的join()(这个方法属于Thread类)方法,正在等待这个方法运行结束。
Terminated(死亡状态、终止状态):线程完成执行后的状态。线程执行完run()方法中的全部代码,从该方法中退出,进入TERMINATED状态。还有一种情况是run()在运行过程中抛出了一个异常,而这个异常没有被程序捕获,导致这个线程异常终止进入TERMINATED状态
isAlive() 判断线程是否还活着,即线程是否还未终止
getPriority() 获得线程的优先级数值
setPriority() 设置线程的优先级数值
Thread.sleep() 将当前线程睡眠指定的毫秒数
join() 调用某线程的该方法,将当前线程与该线程”合并”,即等待该线程结束,再回复当前线程的运行
yield() 让出 CPU ,当前线程进入就绪队列等待调度
wait() 当前线程进入对象的wait pool
notify()/notifyAll() 唤醒对象的wait pool中的一个/所有 等待线程
sleep()与wait()的区别
• 来自不同的类,分别是Thread和Object
• sleep 不释放锁 wait释放锁
• wait、notify、notifyAll只能用在同步方法或同步块中,sleep可以在任何地方使用
• sleep 必须捕获InterruptedException异常,wait、notify、notifyAll不需要捕获
【1】Thread.sleep()
public class ThreadTest {
static class ThreadSleep implements Runnable{
public void run() {
while(true){
System.out.println(new Date());
try {
Thread.sleep(1000);//每个一秒打印一下当前时间
} catch (InterruptedException e) {
e.printStackTrace();
return;//捕捉到InterruptedException异常直接返回
}
}
}
}
public static void main(String[] args){
Thread th = new Thread(new ThreadSleep());
th.start();
try {
Thread.sleep(5000);//main线程睡眠5秒
} catch (InterruptedException e) {
e.printStackTrace();
}
th.interrupt();//打断th线程
}
}
输出结果:
Sun Feb 21 14:33:39 CST 2016
Sun Feb 21 14:33:40 CST 2016
Sun Feb 21 14:33:41 CST 2016
Sun Feb 21 14:33:42 CST 2016
Sun Feb 21 14:33:43 CST 2016
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.test.ThreadTest$ThreadSleep.run(ThreadTest.java:11)
at java.lang.Thread.run(Thread.java:745)
【2】join()
public class ThreadTest {
static class ThreadJoin implements Runnable{
public void run() {
for(int i=0; i<4; i++){
try {
Thread.sleep(1000);//每隔1秒打印一次
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("I am t thread!");
}
}
}
public static void main(String[] args){
Thread t = new Thread(new ThreadJoin());
t.start();
try {
t.join();//当线程 t 执行结束后才执行 main线程
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0; i<3; i++){
System.out.println("I am main thread!");
}
}
}
输出结果:
I am t thread!
I am t thread!
I am t thread!
I am t thread!
I am main thread!
I am main thread!
I am main thread!
【3】yield()
public class ThreadTest {
static class ThreadYield implements Runnable{
public void run() {
for(int i=0; i<=50; i++){
System.out.println(Thread.currentThread().getName()+":"+i);
//当 i能被10整除 时,当前线程让出CPU让同优先级的线程执行(或者自己执行,谁抢到谁执行)
if(i%10 == 0){
Thread.yield();
}
}
}
}
public static void main(String[] args){
Thread t1 = new Thread(new ThreadYield(),"t1");
Thread t2 = new Thread(new ThreadYield(),"t2");
t1.start();
t2.start();
}
}
【4】打断一个线程
public class TestShutDown {
static class MyThread implements Runnable{
private boolean flag = true;//标志位,是否打断 true 执行 false 打断
public void run() {
int i = 0;
while(flag == true){
System.out.print(" " + i++);
}
}
public void shutDown(){
flag = false;
}
}
public static void main(String[] args){
MyThread myThread = new MyThread();
Thread t = new Thread(myThread);
t.start();
for(int i=0;i<=50;i++){
if(i%10==0)
System.out.println("In thread main i="+i);
}
System.out.println("Thread main is over !");
myThread.shutDown();//打断线程
}
}
输出结果:
In thread main i=0
0In thread main i=10
1In thread main i=20
2In thread main i=30
3 4In thread main i=40
5In thread main i=50
6Thread main is over !
7
四、 线程的同步与锁
当多个线程访问同一个对象,非常容对数据对象进行破坏。如下模拟购票的程序
/**
* 多个购票者(多个线程)来售票厅 TicketLobby买票
* 对于购票者,他们共享着TicketLobby 中的ticket,如果不加任何限制同时买票的话,会出现问题
*/
public class ThreadTest {
static class TicketLobby{
private int ticket;
public TicketLobby(int ticket){
this.ticket = ticket;
}
public void saleTicket(){
System.out.println(Thread.currentThread().getName()+"购票1张"+"--剩余票数:" + --ticket);
}
}
static class TicketBuyer implements Runnable{
private TicketLobby ticketLobby;
public TicketBuyer(TicketLobby ticketLobby){
this.ticketLobby = ticketLobby;
}
public void run() {
while(ticketLobby.ticket > 0){
ticketLobby.saleTicket();
try {
Thread.sleep(1000); // 当前线程睡眠一秒,增大出现问题的几率
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args){
TicketLobby ticketLobby = new TicketLobby(10);
Thread t1 = new Thread(new TicketBuyer(ticketLobby),"Buyer1");
Thread t2 = new Thread(new TicketBuyer(ticketLobby),"Buyer2");
Thread t3 = new Thread(new TicketBuyer(ticketLobby),"Buyer3");
t1.start();
t2.start();
t3.start();
}
}
输出结果:
Buyer3购票1张--剩余票数:7
Buyer1购票1张--剩余票数:7
Buyer2购票1张--剩余票数:7
Buyer1购票1张--剩余票数:5
Buyer3购票1张--剩余票数:5
Buyer2购票1张--剩余票数:4
Buyer3购票1张--剩余票数:3
Buyer1购票1张--剩余票数:3
Buyer2购票1张--剩余票数:2
Buyer1购票1张--剩余票数:0
Buyer3购票1张--剩余票数:0
问题的解决方案就是使线程同步,同一时间只能有一个购票者买票,使用synchronized或者使用同步锁Lock,所有对象都自动含有单一的锁(也称为监视器)
(1)synchronized
synchronized有两种表方式:
① 同步代码块
synchronized (object) {
//同步代码
}
② 同步方法
public synchronized void doSomthing() {
//取钱操作
}
如上的购票修改一下就可以保证线程同步
static class TicketLobby{
private int ticket;
private Object waiting = new Object();
public TicketLobby(int ticket){
this.ticket = ticket;
}
public void saleTicket(){
// 使用waiting作为锁的对象,也可使用this对象 synchronized (this)
synchronized (waiting) {
ticket--;
System.out.println(Thread.currentThread().getName()+"购票1张"+"--剩余票数:"+ticket);
}
}
}
或者改成同步方法
public synchronized void saleTicket(){
ticket--;
System.out.println(Thread.currentThread().getName()+"购票1张"+"--剩余票数:"+ticket);
}
输出结果:
Buyer1购票1张--剩余票数:9
Buyer3购票1张--剩余票数:8
Buyer2购票1张--剩余票数:7
Buyer1购票1张--剩余票数:6
Buyer2购票1张--剩余票数:5
Buyer3购票1张--剩余票数:4
Buyer1购票1张--剩余票数:3
Buyer2购票1张--剩余票数:2
Buyer3购票1张--剩余票数:1
Buyer1购票1张--剩余票数:0
任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定(获得锁),程序无法显式的释放对同步监视器的锁定,线程可通过以下方式释放锁定:
① 同步方法、同步代码块执行结束
② 同步方法、同步代码块遇到break、return终止代码的运行
③ 同步方法、同步代码块遇到未处理的Error、Exception
④ 同步方法、同步代码块中,程序执行了同步监视器对象的wait方法
注:程序调用Thread.sleep()/Thread.yield()方法,不会释放同步监视器
(2)同步锁Lock
Lock对象必须被显式的创建、锁定和释放
Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象,使用该Lock对象可以显示加锁、释放锁。不过某些锁支持共享资源的并发访问,如:ReadWriteLock(读写锁)
在线程安全控制中,通常使用ReentrantLock(可重入锁)。ReentrantLock 与synchronized有相同的并发性和内存语义,还包含了中断锁等候和定时锁等候,意味着线程A如果先获得了对象obj的锁,那么线程B可以在等待指定时间内依然无法获取锁,那么就会自动放弃该锁
由于synchronized是在JVM层面实现的,因此系统可以监控锁的释放与否,而ReentrantLock使用代码实现的,系统无法自动释放锁,需要在代码中finally子句中显式释放锁lock.unlock();
在并发量比较小的情况下,使用synchronized是个不错的选择,但是在并发量比较高的情况下,其性能下降很严重,此时ReentrantLock是个不错的方案
static class TicketLobby{
private int ticket;
private final Locklock = new ReentrantLock();
public TicketLobby(int ticket){
this.ticket = ticket;
}
public void saleTicket(){
lock.lock();
try{
ticket--;
System.out.println(Thread.currentThread().getName()+"购票1张"+"--剩余票数:"+ticket);
} finally {
lock.unlock();
}
}
}
【1】使用Synchronized和wait()以及notify()协作完成线程同步
经典的生产者消费者问题,比如有一个容器来盛放苹果,生产者往容器里添加push,消费者从容器中拿取pop,这两个操作需要同步具体代码如下
public class ProducerConsumer {
public static void main(String[] args) {
Containter containter = new Containter();
Thread proT = new Thread(new Producer(containter), "生产者");
Thread conT = new Thread(new Consumer(containter), "消费者");
proT.start();
conT.start();
}
// 生产者和消费者所共享的容器对象(即线程共享的对象)
// 有两个同步方法一个是往里放,一个是往外拿
static class Containter{
int index = 0; // 当前容器大小
Apple[] apples = new Apple[6];
// synchronized 放在方法声明处,会首先去获取当前对象的锁即 this 的锁
public synchronized void push(Apple apple){
// 如果容器满了
while(index == apples.length){
try {
// 则等待,唤醒消费者消费
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 唤醒一个消费者消费
this.notify();
apples[index] = apple;
System.out.println(Thread.currentThread().getName() + " 生产一个苹果 " + apple.toString());
index++;
}
public synchronized Apple pop(){
// 如果容器空了
while(index == 0){
try {
// 等待,唤醒生产者生产
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notify();
index--;
System.out.println(Thread.currentThread().getName() + " 消费了一个苹果 " + apples[index].toString());
return apples[index];
}
}
// 生产者
static class Producer implements Runnable{
private Containter containter;
public Producer(Containter containter){
this.containter = containter;
}
@Override
public void run() {
int i=0;
while(true){
Apple apple = new Apple(i++);
containter.push(apple);
try {
Thread.sleep((int)(Math.random()*1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 消费者
static class Consumer implements Runnable{
private Containter containter;
public Consumer(Containter containter){
this.containter = containter;
}
@Override
public void run() {
while(true){
containter.pop();
try {
Thread.sleep((int)(Math.random()*1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class Apple{
private int id;
public Apple(int id){
this.id = id;
}
public String toString(){
return "Apple: " + id;
}
}
}
输出结果:
生产者 生产一个苹果 Apple: 0
消费者 消费了一个苹果 Apple: 0
生产者 生产一个苹果 Apple: 1
消费者 消费了一个苹果 Apple: 1
生产者 生产一个苹果 Apple: 2
消费者 消费了一个苹果 Apple: 2
生产者 生产一个苹果 Apple: 3
消费者 消费了一个苹果 Apple: 3
生产者 生产一个苹果 Apple: 4
生产者 生产一个苹果 Apple: 5
生产者 生产一个苹果 Apple: 6
消费者 消费了一个苹果 Apple: 6
生产者 生产一个苹果 Apple: 7
生产者 生产一个苹果 Apple: 8
生产者 生产一个苹果 Apple: 9
消费者 消费了一个苹果 Apple: 9
... ...
【2】使用Lock完成线程间的协作
在java.util.concurrent.locks 包中,Condition接口将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用
// 生产者和消费者所共享的容器对象(即线程共享的对象)
// 有两个同步方法一个是往里放,一个是往外拿
static class Containter{
int index = 0; // 当前容器大小
Apple[] apples = new Apple[6];
private final Lock lock = new ReentrantLock();
private final Condition full = lock.newCondition(); // 容器满了的条件变量
private final Condition empty = lock.newCondition(); // 容器空了的条件变量
// synchronized 放在方法声明处,会首先去获取当前对象的锁即 this 的锁
public void push(Apple apple){
lock.lock();
try{
// 如果容器满了
while(index == apples.length){
try {
// 则等待,唤醒消费者消费
full.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 唤醒一个等待线程消费者消费
empty.signal();
apples[index] = apple;
System.out.println(Thread.currentThread().getName() + " 生产一个苹果 " + apple.toString());
index++;
} finally {
lock.unlock();
}
}
public Apple pop(){
lock.lock();
try{
// 如果容器空了
while(index == 0){
try {
// 等待,唤醒生产者生产
empty.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
full.signal();
index--;
System.out.println(Thread.currentThread().getName() + " 消费了一个苹果 " + apples[index].toString());
return apples[index];
} finally {
lock.unlock();
}
}
}
五、线程死锁
死锁发生在两个线程对一对同步对象有循环依赖关系时,哲学家就餐问题就是一个典型的死锁问题。
五个哲学家,五根筷子,围坐在桌子周围吃饭,每人之间放一根筷子,当一个哲学家要就餐时,就必须同时得到左边和右边的筷子,如果一个哲学家左边或右边已经有人在使用筷子了,那么这个哲学家就必须等待,直至得到必需的筷子。
怎么避免死锁
待续… …
Java多线程编程总结:http://lavasoft.blog.51cto.com/62575/27069
15个高级Java多线程面试题及回答:http://www.jb51.net/article/50425.htm
Java线程面试题 Top 50:http://www.importnew.com/12773.html
java多线程试题 :http://blog.csdn.net/u011659172/article/details/17527125
关于java多线程 :http://blog.csdn.net/huaweitman/article/details/41781129
http://blog.csdn.net/huaweitman/article/details/41788479
Java Thread 多线程同步、锁、通信:http://www.cnblogs.com/hoojo/archive/2011/05/05/2038101.html
synchronized 与 Lock 的那点事 :http://www.cnblogs.com/benshan/p/3551987.html