面试中被问的最多的问题之一就是Java的并发与多线程了,一直想花点时间好好总结一下,今天终于下笔。
一、几个概念:
进程:简而言之就是进行中的程序
线程:是“进程”中某个单一顺序的控制流,是程序中一条独立的执行路径
单线程:整个进程中就只有一个单一的线程贯穿始终
多线程:一个进程中含多个线程,cpu在不同的线程中快速地切换执行
二、如何创建一个线程:
继承Thread类
具体步骤:
a. 继承Thread类
b. 复写run()方法
c. 用该类的对象.start()方法来创建一个线程并调用run()方法
注意:不要直接调用run方法,如果直接调用该对象的run方法,跟普通的函数调用没有区别,不能达到开启多线程的目的实现Runnable接口
具体步骤:
a. 实现Runnable接口
b. 复写run()方法
c. 以该类的对象为Thread类构造方法的参数建立一个Thread对象
d. 调用start()方法
示例程序:
package wintervacation.multithreading;
/**
* Created by wangw on 2016/3/2.
* 演示创建多线程的两种方式:
* a.继承Thread类
* b.实现Runnable接口
*/
public class MultiThreadingDemo {
public static void main(String[] args) {
MyThread0 thread0 = new MyThread0();
thread0.start();
/*
myThread1不能作为线程启动运行,必须包装在Thread对象thread1中
但是thread1启动运行的线程是myThread1,执行的线程体也是myThread1的
*/
MyThread1 myThread1 = new MyThread1();
Thread thread1 = new Thread(myThread1);
thread1.start();
for(int i=0;i<500;i++) {
System.out.println(Thread.currentThread().getName()+" is running-------"+i);
}
System.out.println(Thread.currentThread().getName()+"is over");
}
//方式1:继承Thread类
static class MyThread0 extends Thread {
@Override
public void run() {
for(int i=0;i<500;i++) {
System.out.println(Thread.currentThread().getName()+" is running-------"+i);
}
System.out.println(Thread.currentThread().getName()+"is over");
}
}
//方式2:实现Runnable接口
static class MyThread1 implements Runnable {
@Override
public void run() {
for(int i=0;i<500;i++) {
System.out.println(Thread.currentThread().getName()+" is running-------"+i);
}
System.out.println(Thread.currentThread().getName()+"is over");
}
}
}
运行结果:
二者的区别:
- Java只允许单继承,第一种方式要求继承Thread类没有其他父类,所以第二种方式的使用范围更广
- 对于多个具有相同程序代码的线程处理同一资源的情况,第二种方式对CPU(线程)、程序的代码和数据进行了有效的分离,较好的体现了面向对象的思想。
请看下面两个程序:
// SellTicketSystem0.java
package wintervacation.multithreading;
/**
* Created by wangw on 2016/3/2.
* 用继承Thread的方式模拟火车票售票系统,与实现Runnable接口的方式作比较
* 假设四个系统同时出售某一车次的100张车票
*/
public class SellTicketSystem0 {
public static void main(String[] args) {
MyThread myThread0 = new MyThread();
myThread0.start();
MyThread myThread1 = new MyThread();
myThread1.start();
MyThread myThread2 = new MyThread();
myThread2.start();
MyThread myThread3 = new MyThread();
myThread3.start();
}
}
class MyThread extends Thread {
private int ticket = 100;
@Override
public void run() {
while(ticket>0) {
System.out.println(this.getName()+" sell out ticket" + ticket--);
}
}
}
运行结果:
可以看到每一张票被每个线程都卖了一遍,原因是每个线程都有它自己的100张票,显然不是我们想要的
再看另一种方式:
// SellTicketSystem1.java
package wintervacation.multithreading;
/**
* Created by wangw on 2016/3/2.
* 用实现Runnable接口的方式模拟火车票售票系统,与继承Thread的方式作比较
* 假设四个系统同时出售某一车次的100张车票
*/
public class SellTicketSystem1 {
public static void main(String[] args) {
MyThread thread = new MyThread();
Thread myThread0 = new Thread(thread);
myThread0.start();
Thread myThread1 = new Thread(thread);
myThread1.start();
Thread myThread2 = new Thread(thread);
myThread2.start();
Thread myThread3 = new Thread(thread);
myThread3.start();
}
static class MyThread implements Runnable {
private int ticket = 100;
@Override
public void run() {
while(ticket>0) {
System.out.println(Thread.currentThread().getName() + " sell out ticket" + ticket--);
}
}
}
}
运行结果:
可以看到,所有的线程访问同一个变量,在不考虑同步的情况下很好的模拟了火车票售票系统。
三、线程状态的转换
线程的生命周期图:
Oracle官方给出的线程的状态分为四个:new, runnable, non-runnable 以及 terminated。
此处我们采用更容易接受的五状态生命周期图。说明如下:
- New
新建状态,线程的实例已经被创建但是start方法尚未被调用 - Runnable
当该线程的start方法被调用,但是还没有被CPU调度选中时,线程处于Runnable状态 - Running
CPU调度选中,获得运行权 - Non-Runnable (Blocked)
线程阻塞状态,线程仍然存活,但是由于某些原因,导致它的运行受阻暂时不能运行 - Terminated
当线程的run方法退出时,线程进入Teminated状态
四、线程的优先级和常用的方法
- 优先级
线程的优先级用1~10之间的一个整数表示,数值越大优先级越高,Thread类中定义了三个常量:
最高优先级:Thread.MAX_PRIORITY————10
最低优先级:Thread.MIN_PRIORITY————1
默认优先级:Thread.NORM_PRIORITY———–5 - 常用方法
public void start(): starts the execution of the thread.JVM calls the run() method on the thread.
public void sleep(long miliseconds): Causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds.
public void join(): waits for a thread to die.
public void join(long miliseconds): waits for a thread to die for the specified miliseconds.
public int getPriority(): returns the priority of the thread.
public int setPriority(int priority): changes the priority of the thread.
public String getName(): returns the name of the thread.
public void setName(String name): changes the name of the thread.
public Thread currentThread(): returns the reference of currently executing thread.
public int getId(): returns the id of the thread.
public Thread.State getState(): returns the state of the thread.
public boolean isAlive(): tests if the thread is alive.
public void yield(): causes the currently executing thread object to temporarily pause and allow other threads to execute.
public void suspend(): is used to suspend the thread(depricated).
public void resume(): is used to resume the suspended thread(depricated).
public void stop(): is used to stop the thread(depricated).
public boolean isDaemon(): tests if the thread is a daemon thread.
public void setDaemon(boolean b): marks the thread as daemon or user thread.
public void interrupt(): interrupts the thread.
public boolean isInterrupted(): tests if the thread has been interrupted.
public static boolean interrupted(): tests if the current thread has been interrupted.
示例程序:
// ThreadPriority.java
package wintervacation.multithreading;
/**
* Created by wangw on 2016/3/2. 用于演示Thread的优先级,高优先级线程的执行优先于低优先级线程
*/
public class ThreadPriority {
public static void main(String[] args) throws InterruptedException {
System.out.println("最高优先级:" + Thread.MAX_PRIORITY);
System.out.println("最低优先级:" + Thread.MIN_PRIORITY);
System.out.println("默认优先级:" + Thread.NORM_PRIORITY);
MyThread thread0 = new MyThread();
thread0.setPriority(Thread.MAX_PRIORITY);
thread0.start();
MyThread thread1 = new MyThread();
thread1.setPriority(Thread.MIN_PRIORITY);
thread1.start();
MyThread thread2 = new MyThread();
thread2.setPriority(Thread.NORM_PRIORITY);
thread2.start();
// join方法:当前线程执行某一线程的join方法之后,当前线程等到该线程执行结束之后才能执行
// thread1.join();
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + " is running-------" + i);
}
System.out.println(Thread.currentThread().getName() + "is over");
}
static class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + " is running-------" + i);
大专栏 语法:多线程ss="token punctuation">}
System.out.println(Thread.currentThread().getName() + "is over");
}
}
}
五、同步问题
临界资源:同一时刻只允许一个线程访问的资源
临界区:处理临界资源的代码
涉及到临界资源的问题需要用到同步机制,否则可能会出现错误或会导致不安全,上面的卖票程序就存在这样的问题,为了使效果更加明显,我们在单个线程卖票之前让它睡眠100ms:
static class MyThread implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " sell out ticket" + ticket--);
}
}
}
运行结果:
我们看到Thread-0,Thread-2和Thread-1分别卖出了代号为0,-1,-2的票,这显然是错误的
原因是线程3检测到ticket=1时,进入睡眠,此时线程0检测ticket仍然为1,也进入临界区,睡眠,同理,线程2,1依次进入临界区,睡眠。
100ms后,Thread-3恢复运行,卖出标号为1的票,ticket减一变为0,线程0,2,1依次恢复运行并执行卖票和ticket–的动作,便出现了图示的结果。
为了保护共享数据的完整性,JAVA语言引入了互斥锁的概念。
互斥锁:Java中的每一个对象都有且仅有一个互斥锁,对于加锁的临界区,只有拿到该锁的线程可以访问,Java使用synchronized关键字给对象家锁。
同步方法:使用synchronized修饰的方法,开发者不用指定加锁对象,JVM默认给this对象加锁
同步代码块:使用synchronized修饰的代码块,需要指定加锁对象
改进后的火车票售票系统:
<方式1>
static class MyThread implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
synchronized ("haha") {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " sell out ticket" + ticket--);
} else
break;
}
}
}
}
<方式2>
static class MyThread implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
sell();
if (ticket <= 0)
break;
}
}
public synchronized void sell() {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " sell out ticket" + ticket--);
}
}
}
运行结果:
六、死锁问题
死锁:并发运行的多个线程彼此等待对方占有的资源、都无法运行的状态
synchronized关键字要慎用,不然很容易出现死锁,下面是一个例子:
// DeadLockDemo.java
package wintervacation.multithreading;
/**
* Created by wangw on 2016/3/2. 死锁的例子
*/
public class DeadLockDemo {
public static void main(String[] args) throws InterruptedException {
StringBuffer sb = new StringBuffer("haha");
MyThread thread = new MyThread(sb);
thread.start();
synchronized (sb) {
thread.join();
System.out.println(Thread.currentThread().getName());
}
}
static class MyThread extends Thread {
private StringBuffer sb = new StringBuffer();
MyThread(StringBuffer sb) {
this.sb = sb;
}
@Override
public void run() {
synchronized (sb) {
System.out.println(this.getName());
}
}
}
}
主线程中调用了thread的join方法,等待thread线程执行完毕,而thread线程需要sb锁,sb锁被main线程占用,故也无法执行,发生死锁。
死锁没有好的解决方式,应该尽量避免。
七、线程之间的通信
多线程程序之间如果没有通信,相互孤立,就失去了多线程的意义了。
通信问题的经典例子:
生产者消费者的同步问题(问题描述请自行上网搜索)
Java多线程实现:
// ConsumerAndProductor.java
package wintervacation.multithreading;
/**
* Created by wangw on 2016/3/2. 消费者和生产者的例子,用于说明线程之间的通信 把生产者和消费者作为两个线程
* 仓库作为一个类,有装入生产者生产的商品和向消费者提供商品两个方法
*/
public class ConsumerAndProductor {
public static void main(String[] args) {
Repo repo = new Repo();
ComsumerThread comsumerThread = new ComsumerThread(repo);
comsumerThread.start();
ProductorThread productorThread = new ProductorThread(repo);
productorThread.start();
}
}
class Repo {
// 仓库可以容纳6件商品
char[] data = new char[6];
int index = 0;
public synchronized void in(char c) {
if (index == 6) {
try {
this.wait();
System.out.println("-----" + this);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
data[index++] = c;
System.out.println("生产了产品" + c);
this.notify();
}
public synchronized char out() {
if (index == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
index--;
System.out.println("消费了产品" + data[index]);
this.notify();
return data[index];
}
}
class ComsumerThread extends Thread {
Repo repo;
ComsumerThread(Repo repo) {
this.repo = repo;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
char c = (char) (Math.random() * 26 + 'A');
repo.in(c);
try {
Thread.sleep((int) (Math.random() * 10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class ProductorThread extends Thread {
Repo repo;
ProductorThread(Repo repo) {
this.repo = repo;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
repo.out();
try {
Thread.sleep((int) (Math.random() * 100));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}