------- android培训、java培训、期待与您交流! ----------
一、多线程概述
1、进程
进程:是操作系统结构的基础,是一个正在执行的程序。如QQ,word等等。
多进程:在同一时刻运行多个程序的能力。如听歌的同时可以聊QQ
2、线程
线程:一个程序同时执行多个任务,通常,每一个任务称为一个线程,线程即一条执行路径,一个执行单元。
多线程:是为了使得多个线程并行的工作以完成多项任务,以提高系统的效率。
3、CPU执行的原理
在听歌的时候可以聊QQ,表面上,CPU在同时处理这些程序,但是实际上却不是这样,CPU一次只能处理一个程序,但是cpu的计算效率非常快,不断切换计算任务,耗时短,造成并行处理的假象。
4、进程与线程区别
多进程与多线程本质区别在于每个进程都有自己的一整套变量,而线程则共享数据。也就是说,QQ有着自己的变量,而听歌也有,他们之间的变量互不影响,但是线程确是共享数据。
二、创建线程的两种方式
1、继承Thread
线程本来就是一类事物,根据面向对象的思想,因此就存在了Thread这个类,通过查找Api帮助文档发现,继承Thread类需要复写run()方法,根据面向对象思想,的确,每个线程的任务不一样,当然需要自己指定任务。
步骤:
(1)写一个类,继承Thread
(2)创建一个线程,Thread t1 = new Thread子类();
(3)开启线程t1.start(),这样线程就开始执行任务。
示例代码如下:
package cn.itheima.blog5;
//继承Thread类
public class TicketThread extends Thread{
/**
* 通过继承Thread类来实现卖票
*/
private int ticket = 100;
//重写run方法,将任务封装起来
public void run(){
while(true){
if(ticket > 0){
System.out.println(ticket--);
}else{
break;
}
}
}
public static void main(String[] args) {
//创建一个线程
Thread t1 = new TicketThread();
//开启一个线程
t1.start();
}
}
2、实现Runnable接口
当某些事物本身有自身的父类,但是又需要通过线程去完成任务,而java又不支持多继承,那该如何是好呢?因此就必须提供一个接口,这个接口便是Runnable,通过实现Runnable接口也可以创建一个线程。
步骤:
(1)class 类名 implements Runnable
(2)复写run方法
(3)创该类对象,类名 对象名 = new 类名();
(4)新建一个Thread对象,将该类对象作为Thread的构造函数参数传入
(5)开启线程任务
示例代码如下:
package cn.itheima.blog5;
public class TicketRunnable implements Runnable{
private int ticket = 100;
/**
* 实现Runnable接口
*/
//复写run方法
@Override
public void run() {
sale();
}
//卖票的方法
public void sale(){
while(true){
if(ticket > 0){
System.out.println(Thread.currentThread().getName() + "----" + ticket--);
}else{
break;
}
}
}
public static void main(String[] args) {
TicketRunnable tr = new TicketRunnable();
Thread t1 = new Thread(tr);
t1.start();
}
}
3、对比总结
当有100张票分为四个窗口卖,那用哪种方式创建线程比较好呢?
我们肯定得创建一个卖票类,任务就是卖票,而且多个窗口卖,明显需要用到多线程,同时还要注意一点,实现的是票数共享。我们先尝试用继承Thread类来尝试一番,发现无论开启多少个线程,都无法模拟实现多个窗口共卖这一种票,而是自个为战,自己买自己的100张票。我们可以这样分析,新建每一个票类对象的时候都附上初始值100,当然会出现各自卖各自的100票,因此我们只能用实现Runnable接口这一方式来实现该功能。
创建线程的两种方式的对比,实现Runnable相比继承Thread类具有如下优势
(1)避免因单继承带来的局限性;
(2)适合多个相同程序代码去处理同一资源;
(3)增强了程序开发的健壮性,代码能够被多个线程共享,数据域代码是独立的。
三、多线程的状态
1、创建状态
在通过构造方法创建一个线程对象后,新的线程便处于创建状态,即Thread t = new Thread();便处于创建状态。
2、就绪状态
当线程调用start()后,便进入了就绪状态,说明线程具有cpu的执行资格,此时线程需要排队,等待cpu执行。
3、运行状态
当就绪状态的线程被调用并获得处理资源时,便进入了运行状态,说明线程具有cpu的执行权。
4、阻塞状态
在可执行状态下,若调用了sleep()、suspend()、wait()等方法,线程进入阻塞状态,阻塞后,线程不能进行排队,只有当阻塞的原因解除后线程才能转入就绪状态。
5、死亡状态
线程调用stop()或者run()方法运行结束,线程便进入死亡状态,此时线程已经不具有继续执行的能力。
四、多线程的安全问题及其解决办法
1、安全威胁
我们来写一段代码,来实现上面卖票的需求,为了方便观察结果,每个线程在卖票前都休眠一段时间。
代码如下:
package cn.itheima.blog5;
public class TicketSafeDemo implements Runnable{
/**
* 卖票线程安全问题演示
*/
private int tickNum = 100;
//封装任务
@Override
public void run() {
sale();
}
//卖票操作
public void sale() {
while(true){
if(tickNum > 0){
try {
//将线程休眠20ms
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖掉了" + tickNum-- + "号票");
}else{
break;
}
}
}
public static void main(String[] args) {
//创建任务对象
TicketSafeDemo ts = new TicketSafeDemo();
//新建三个线程
Thread t0 = new Thread(ts);
Thread t1 = new Thread(ts);
Thread t2 = new Thread(ts);
//开启三个线程
t0.start();
t1.start();
t2.start();
}
}
运行结果:
一个令人不解的问题出现了,既然是卖100张票,为什么还卖到了0号,甚至是-1号票呢,这样票不是多了吗?在这,其实就出现了多线程操作共享数据的时候产生的安全威胁。那又是为什么出现安全威胁呢?
我们来读一读代码,设票最后还剩下一张,0线程进去,判断,成功,进入循环,一进去,休眠;然后1线程进去,判断,又去上面的票数没减,判断依然成功;线程2如线程1一般,当线程都苏醒的时候,直接往下走,不进行判断,这样就出现了0,甚至-1号票的情况。
安全威胁出现的原因:多条语句操作共享数据
2、解决方案--同步
既然出现了问题,我们就需要解决。我们有一个想法,就是加锁。如火车上上厕所,一进门,就关上门上锁,别人无法进来,只能等进去的人出来,别人才能进去。移植到java中,我们是不是也能加上一个锁,当有线程访问数据的时候,其他线程无法进入操作数据,这样问题就解决了。
方案一:同步代码块
引入对象锁的概念,以及关键字synchronized,用法为synchronzied(对象){需要被同步的代码};
package cn.itheima.blog5;
public class TicketSynchronzied implements Runnable {
/**
* 卖票线程安全问题解决方案之同步代码块
*/
private int tickNum = 100;
Object obj = new Object();
// 封装任务
@Override
public void run() {
sale();
}
// 卖票操作
public void sale() {
while (true) {
synchronized (obj) {
if (tickNum > 0) {
try {
// 将线程休眠20ms
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖掉了"
+ tickNum-- + "号票");
}else{
break;
}
}
}
}
public static void main(String[] args) {
// 创建任务对象
TicketSynchronzied ts = new TicketSynchronzied();
// 新建三个线程
Thread t0 = new Thread(ts);
Thread t1 = new Thread(ts);
Thread t2 = new Thread(ts);
// 开启三个线程
t0.start();
t1.start();
t2.start();
}
}
方案二:同步函数
在函数声明中添加synchronized,非静态函数的对象锁的对象为this,而静态函数的对象锁的对象为this.class。
package cn.itheima.blog5;
public class TicketSynFunction implements Runnable{
/**
* 卖票线程安全问题演示
*/
private int tickNum = 100;
//封装任务
@Override
public void run() {
sale();
}
//卖票操作
public synchronized void sale() {
while(true){
if(tickNum > 0){
try {
//将线程休眠20ms
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖掉了" + tickNum-- + "号票");
}else{
break;
}
}
}
public static void main(String[] args) {
//创建任务对象
TicketSynFunction ts = new TicketSynFunction();
//新建三个线程
Thread t0 = new Thread(ts);
Thread t1 = new Thread(ts);
Thread t2 = new Thread(ts);
//开启三个线程
t0.start();
t1.start();
t2.start();
}
}
同步的好处与弊端
好处:解决了安全威胁
弊端:需要判断锁,浪费资源,使程序效率变慢
同步的前提
当安全威胁存在,我们常用的方法为加锁,但是,如果加上锁,威胁依然没有消失,那么我们就必须去考虑同步的前提;也就是多个线程是否用同一个锁。
3、同步引发的问题--死锁
死锁:是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。通常是因为锁的嵌套而产生的。死锁程序代码示例如下:
package cn.itheima.blog5;
public class ThreadDeadLock implements Runnable {
/**
* 死锁示例
*/
private boolean flag;
private ThreadDeadLock(boolean flag) {
super();
this.flag = flag;
}
@Override
public void run() {
if (flag) {
while (true) {
synchronized (MyLock.locka) {
System.out.println(Thread.currentThread().getName()
+ "---if---locka---");
synchronized (MyLock.lockb) {
System.out.println(Thread.currentThread().getName()
+ "---if---lockb---");
}
}
}
} else {
while (true) {
synchronized (MyLock.lockb) {
System.out.println(Thread.currentThread().getName()
+ "---else---lockb---");
synchronized (MyLock.locka) {
System.out.println(Thread.currentThread().getName()
+ "---else---locka---");
}
}
}
}
}
public static void main(String[] args) {
ThreadDeadLock deadLock1 = new ThreadDeadLock(false);
ThreadDeadLock deadLock2 = new ThreadDeadLock(true);
Thread t1 = new Thread(deadLock1);
Thread t2 = new Thread(deadLock2);
t1.start();
t2.start();
}
}
class MyLock {
public static final Object locka = new Object();
public static final Object lockb = new Object();
}
运行结果为:
五、线程之间的通信
多线程的通信:多个线程操作同一资源,但是任务却不同,如生产者与消费者,生产者的任务是生产,而消费者的任务是消费,虽然他们操作的都是商品这一资源。
需求:生产一个商品,就等消费者消费,消费完这一商品后,才能继续生产。
出现这一需求我们就要加入等待唤醒机制,用生产者与消费者的例子来说,等待唤醒机制就是生产者生产完一个商品后,就叫消费者来消费;等消费者消费完以后,就叫生产者来生产。下面写一段代码来实现该需求:
package cn.itheima.blog5;
/*
* 线程通信之等待唤醒机制(以生产者消费者为例)
* 有资源,为水
* 一个线程生产水,另一个线程取走水
* 生产一瓶水,就消费一瓶水;若没有水,则等待生产
*/
//共享资源为水
class Water {
private String brand;
private String description;
private boolean flag = false;
public synchronized void set(String brand, String description) {
//如果有资源则等待取出
if (flag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//无资源则生产水
this.brand = brand;
this.description = description;
//生产完水后,将标志位设定为有水
flag = true;
//唤醒等待的线程
notify();
}
public synchronized void get(){
//若没水,则等待生产;若有,则取走
if(!flag){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(brand + "..." + description);
//将标志置为没水
flag = false;
//唤醒等待线程
notify();
}
}
class Producer implements Runnable {
private Water r;
public Producer(Water r) {
this.r = r;
}
@Override
public void run() {
//交替生产两种水
int x = 0;
while(true){
if(x == 0){
r.set("润田", "好喝");
}else{
r.set("wahaha", "woxihuan");
}
x = (x + 1) % 2;
}
}
}
class Customer implements Runnable {
private Water r;
public Customer(Water r) {
this.r = r;
}
@Override
public void run() {
while(true){
r.get();
}
}
}
public class WaitAndNotify {
public static void main(String[] args) {
//新建资源
Water r = new Water();
//新建任务
Producer pro = new Producer(r);
Customer cust = new Customer(r);
//新建执行路径
Thread t1 = new Thread(pro);
Thread t2 = new Thread(cust);
//开启执行
t1.start();
t2.start();
}
}
上述代码只是解决了单个消费者,单生产者的问题,但是,若生产者与消费者均是多个,问题又会出现一个产品被消费几次,或者一个产品被生产几次的情况。代码如下:
package cn.itheima.blog5;
class Product_1{
private String name;
private int count = 0;
private boolean flag = false;
public synchronized void set(String name){
//有产品,等待
if(flag){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name + count++;
flag = true;
System.out.println(Thread.currentThread().getName() + "...生产了..." + this.name);
notify();
}
public synchronized void get(){
//没产品,取走
if(!flag){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "消费了" + name);
flag = false;
notify();
}
}
class Input_1 implements Runnable{
private Product_1 p;
public Input_1(Product_1 p){
this.p = p;
}
@Override
public void run() {
while(true){
p.set("商品");
}
}
}
class Output_1 implements Runnable{
private Product_1 p;
public Output_1(Product_1 p){
this.p = p;
}
@Override
public void run() {
while(true)
p.get();
}
}
public class MoreProCust_1 {
/**
* @param args
*/
public static void main(String[] args) {
//产品
Product_1 p = new Product_1();
//两个线程操作同一任务--生产
Input_1 input = new Input_1(p);
Thread t1 = new Thread(input);
Thread t2 = new Thread(input);
//两个线程操作同一任务--消费
Output_1 out = new Output_1(p);
Thread t3 = new Thread(out);
Thread t4 = new Thread(out);
//开启任务
t1.start();
t2.start();
t3.start();
t4.start();
}
}
运行结果:
产生此种现象的因素主要有两点:
1、于在判断标志位的时,线程若进入临时阻塞状态,后面唤醒后不会进行判断;
2、唤醒线程的不确定性
解决方案一:
用while循环与notifyAll()改良,具体代码如下:
package cn.itheima.blog5;
class Product_1{
private String name;
private int count = 0;
private boolean flag = false;
public synchronized void set(String name){
//有产品,等待
while(flag){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name + count++;
flag = true;
System.out.println(Thread.currentThread().getName() + "...生产了..." + this.name);
notifyAll();
}
public synchronized void get(){
//没产品,取走
while(!flag){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "消费了" + name);
flag = false;
notifyAll();
}
}
class Input_1 implements Runnable{
private Product_1 p;
public Input_1(Product_1 p){
this.p = p;
}
@Override
public void run() {
while(true){
p.set("商品");
}
}
}
class Output_1 implements Runnable{
private Product_1 p;
public Output_1(Product_1 p){
this.p = p;
}
@Override
public void run() {
while(true)
p.get();
}
}
public class MoreProCust_1 {
/**
* @param args
*/
public static void main(String[] args) {
//产品
Product_1 p = new Product_1();
//两个线程操作同一任务--生产
Input_1 input = new Input_1(p);
Thread t1 = new Thread(input);
Thread t2 = new Thread(input);
//两个线程操作同一任务--消费
Output_1 out = new Output_1(p);
Thread t3 = new Thread(out);
Thread t4 = new Thread(out);
//开启任务
t1.start();
t2.start();
t3.start();
t4.start();
}
}
解决方案二: jdk1.5新特性
1.5以前的对象锁是一种隐式操作。根据面向对象思想,锁也是对象,因此就将锁进行封装,同时再此基础上增加监视器,突破以前一个锁只有一个监视器的局限性,将Object
监视器方法(wait
、notify
和notifyAll
)分解成截然不同的对象,以便通过将这些对象与任意 Lock
实现组合使用。现用jdk1.5的新特性来解决该问题,代码如下:
package cn.itheima.blog5;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 多生产者,多消费者的JDK1.5解决方案
*/
class Product_2 {
private String name;
private int count = 0;
private boolean flag = false;
//得到锁对象
private Lock lock = new ReentrantLock();
//分别创建生产者与消费者对应于该锁的监视器
Condition input_con = lock.newCondition();
Condition output_con = lock.newCondition();
public void set(String name) {
//加锁
lock.lock();
try {
while (flag) {
try {
//生产者监视器控制的线程进入临时阻塞状态
input_con.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name + count++;
flag = true;
System.out.println(Thread.currentThread().getName() + "...生产了..."
+ this.name);
//唤醒消费者监视器控制的线程
output_con.signal();
} finally {
//为防止锁无法释放,因此放在finally块中,释放锁
lock.unlock();
}
}
public void get() {
//加锁
lock.lock();
try{
while (!flag) {
try {
//消费者监视器控制的线程进入临时阻塞状态
output_con.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "消费了" + name);
flag = false;
//唤醒生产者监视器控制的线程
input_con.signal();
}finally{
//释放锁
lock.unlock();
}
}
}
class Input_2 implements Runnable{
private Product_2 p;
public Input_2(Product_2 p){
this.p = p;
}
@Override
public void run() {
while(true){
p.set("商品");
}
}
}
class Output_2 implements Runnable{
private Product_2 p;
public Output_2(Product_2 p){
this.p = p;
}
@Override
public void run() {
while(true)
p.get();
}
}
public class MoreProCust_2 {
public static void main(String[] args) {
Product_2 p = new Product_2();
Input_2 input = new Input_2(p);
Thread t1 = new Thread(input);
Thread t2 = new Thread(input);
Output_2 out = new Output_2(p);
Thread t3 = new Thread(out);
Thread t4 = new Thread(out);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
刚开始对多线程技术的学习,的确有丈二和尚,摸不着头脑的感觉,但是毕老师有一句话可以解决很多问题,那就是线程其实就是一条执行路径,通过这句话我们就可以分析许多线程中出现的问题,然后一步步解决。