多线程总结
通过近期对多线程的初步学习,总结下所学的一些东西,部分内容转载自http://blog.csdn.net/shimiso,感谢博主的分享
多线程概念
现在的操作系统是多任务操作系统。多线程是实现多任务的一种方式。
进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。比如在Windows系统中,一个运行的exe就是一个进程。
线程是指进程中的一个执行流程,一个进程中可以运行多个线程。比如java.exe进程中可以运行很多线程。线程总是属于某个进程,进程中的多个线程共享进程的内存。
“同时”执行是人的感觉,在线程之间实际上轮换执行。这些线程被cpu随机切换着执行,由于cpu运算速度非常快,而且切换的频率很高,所以看起来多个线程就像是同时运行的。
线程的创建
线程创建有两种方式,无论是哪一种,都包含run()方法,线程的任务就定义在run()方法中
一、继承Thread类
public class DemoThread extends Thread{
//重写其run()方法
public void run(){
for(int i=0;i<10000;i++){
System.out.println(i);
}
}
}
public static void main(String[] args){
Thread t1 = new DemoThread();//创建线程子类对象
t1.start();//调用start()方法
}
继承Thread类表明你要继承线程体系,除非需要继承线程体系,否则建议使用第2种方法。注意,java不支持多继承,所以如果该类已经有父类了,则只能使用第2种方法。
二、实现Runnable接口
public class DemoThread implements Runnable{
//实现其run()方法
public void run(){
for(int i=0;i<10000;i++){
System.out.println(i);
}
}
}
public static void main(String[] args){
DemoThread dt1 = new DemoThread();//创建线程任务对象
Thread t1 = new Thread(dt1);//创建线程对象,将线程任务作为参数传入
t1.start();//调用start()方法
}
线程的状态及常用方法
一、线程的状态
线程的状态转换时线程控制的基础。线程状态可分为五种:
1.新建状态:线程刚被建立,还没有调用start()方法之前。此时线程既没有执行权也没有执行资格。
2.可运行状态:当新建的线程调用start()方法后,线程进入可运行状态,此时线程拥有执行资格,可以被cpu执行,但是没有执行权。
3.运行状态:当cpu切换到了一个线程,我们说这个线程获得了cpu的执行权,此时线程处于运行状态。
4.等待/睡眠/阻塞/冻结状态:在线程运行过程中,由于一些事件的出现,它可能进入冻结状态,释放掉执行权和执行资格。同时,随着一些事件的发生,它可能返回可运行状态。
5.消亡状态:当线程的run()方法执行完毕时,线程就进入了消亡状态。当线程发生了未捕获的异常终止了run()方法时,线程也会进入消亡状态。
二、线程方法
1.获取线程-currentThread
Thread.currentThread():用于获取当前运行线程对象
2.睡眠-sleep
Thread.sleep(long millis):使当前线程进入冻结状态一段时间,期间线程不会被执行,当睡眠时间结束,线程恢复可运行状态。
3.让步-yield
Thread.yield():暂停当前正在执行的线程对象,并执行其他线程。即释放掉执行权,让该线程恢复可运行状态。
4.加入-join
t.join():临时加入一个线程,在该线程执行完毕前,当前线程暂时释放执行权和执行资格,进入冻结状态,其他线程任务不受影响。该方法通常用于临时运算,例如:
public class JoinDemo implements Runnable {
public void run() {
for(int i=1;i<=100;i++){
System.out.println(Thread.currentThread().getName()+"..."+i);
}
}
}
public static void main(String[] args) {
JoinDemo jd = new JoinDemo();
Thread t0 = new Thread(jd);
Thread t1 = new Thread(jd);
t1.start();
t0.start();
try {
t0.join();//t0加入到main线程中
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=1;i<=100;i++){
System.out.println(Thread.currentThread().getName()+"..."+i);
}
}
执行结果如下
Thread-1...68
Thread-0...98
Thread-0...99
Thread-0...100
Thread-1...69
Thread-1...70
Thread-1...71
Thread-1...72
main...1
Thread-1...73
main...2
Thread-1...74
main...3
Thread-1...75
main...4
可以看出t1和t0两个线程轮换执行,直到t0执行完毕后main线程才开始参与执行,与t1轮换执行。
5.设置优先级-setPriority
t.setPriority(int newPriority):线程的优先级决定了线程被执行的概率,优先级分为1-10,默认的优先级为5。使用setPriority方法可以调整线程的优先级。通常使用两个常量MAX_PRIORITY、和MIN_PRIORITY来进行优先级的设置,这两个常量分别代表优先级10和1.
6.守护线程-setDaemon
t.setDaemon(true):使用该方法来将线程设置为守护线程(后台线程),当正在运行的线程都是守护线程时,程序结束。我们知道正常情况下程序中如果有线程在运行的话程序是不会结束的,如果我们希望让一个任务在程序结束后依然可以运行,那么可以将它设为守护线程。
7.中断-interrupt
t.interrupt():当线程处于冻结状态时,程序无法结束,此时可以使用interrupt()方法将线程从冻结状态强制恢复到运行状态中来,让线程具备CPU的执行资格,但是强制动作会发生InterruptedException,记得要处理
用法:当运行中的线程处于冻结状态,而程序准备结束时,使用interrupt使其强制结束
线程同步和同步锁
一、为什么要同步
当多个线程同时操作同个资源,且操作语句不止一条时,便会出现线程安全问题,如:
public class Ticket implements Runnable {
private int num = 100;
@Override
public void run() {
while(true){
if(num>0){
try {
Thread.sleep(1);//为了使效果更明显,进行了1ms的睡眠
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"...sale..."+num--);
}
}
}
}
//4个线程同时进行买票动作
public static void main(String[] args) {
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
}
执行结果如下
//结果出现了不正确的数据,如重复的2,以及不该卖出的0,-1,-2号票
Thread-0...sale...6
Thread-2...sale...5
Thread-1...sale...7
Thread-1...sale...4
Thread-0...sale...2
Thread-2...sale...2
Thread-3...sale...3
Thread-1...sale...1
Thread-0...sale...-2
Thread-2...sale...-1
Thread-3...sale...0
造成这种现象的原因是1个线程通过num>0的判断后,还未进行num–时另外1个线程获得了执行权,并通过了num>0的判断,这个线程再次获得执行权时,已经跳过了安全性的判断,于是出现了错误的数据。
二、同步和锁定
java提供了解决上面问题的方式就是同步,使用关键字synchronized。同步有2种方式,一种是同步函数,一种是同步代码块,如:
public class Ticket implements Runnable {
private int num = 100;
public void run() {
while(true){
synchronized (this) {//加入同步代码块
if(num>0){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"...sale..."+num--);
}
}
}
}
}
public class Ticket implements Runnable {
private int num = 100;
public void run() {
while(true){
show();
}
}
//同步函数
public synchronized void show(){
if(num>0){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"...sale..."+num--);
}
}
}
java中每个对象都有一个内置锁,同步就是借助对象的内置锁来完成的,同步代码块需要传入一个对象,二同步函数的对象就是this,注意,静态同步函数的锁对象是该类对象(XXX.CLASS)。线程在执行同步方法或同步代码块时会获得该对象的锁,一个对象只有一个锁,所以其他线程无法进入同步代码块或同步方法知道锁被释放,即持有锁的线程执行完同步代码块或同步方法。
三、死锁现象
死锁现象是多线程过程中可能出现的一个问题,当两个线程都处于阻塞状态,互相等待对方,或者都进入了冻结状态,无法被唤醒,这种现象就成为死锁现象。
看一个简单的死锁的例子
public class DeadLock implements Runnable{
private boolean flag;
public static Object locka = new Object();
public static Object lockb = new Object();
public DeadLock(boolean flag) {
super();
this.flag = flag;
}
public void run() {
if(flag){//一个线程执行下面代码
while(true){
synchronized (locka) {
System.out.println(Thread.currentThread().getName()+"...if...locka");
synchronized (lockb) {
System.out.println(Thread.currentThread().getName()+"...if...lockb");
}
}
}
}else{//另一个线程执行下面的代码
while(true){
synchronized (lockb) {
System.out.println(Thread.currentThread().getName()+"...else...lockb");
synchronized (locka) {
System.out.println(Thread.currentThread().getName()+"...else...locka");
}
}
}
}
}
}
public static void main(String[] args) {
DeadLock d1 = new DeadLock(true);
DeadLock d2 = new DeadLock(false);
Thread t1 = new Thread(d1);
Thread t2 = new Thread(d2);
t1.start();
t2.start();
}
一次执行结果如下
Thread-0...if...locka
Thread-1...else...lockb
在上述例子中,一个线程持有了locka,另一个线程持有了lockb,两个线程都在等待对方释放锁,结果就出现了死锁现象。实际上,死锁发生的概率很低,但是,一旦发生死锁程序就会死掉。所以,应该尽量避免
四、同步的小结
1.线程安全问题出现的2个原因:
(1)多个线程同时操作操作共享的数据
(2)操作共享数据的代码有多条
2.同步通过锁来实现,每个对象都有且仅有一个锁,线程一旦获取了锁,其他线程就无法访问该锁绑定的同步方法。
3.同步方法的锁是正在执行该方法的当前实例(即this),静态同步方法的锁是该类对象。
4.同步的关键是搞清楚在哪个对象上同步,即弄明白哪些代码绑定了同一个锁。
5.多线程是比较容易出现错误的技术,在编写代码时要特别注意线程安全问题和死锁。
线程的交互
一、监视器方法
多线程通常会出现多个线程对资源进行不同操作的场景,这时候通常一个线程需要等待另外一个线程对资源操作完毕以后,才能对资源进行操作,这时候就需要等待和唤醒方法。
前面说到同步锁是通过对象来完成的,同步锁也称为监视器。而接下来这三个方法就是object类提供的监视器方法
wait():使当前线程进入等待状态
notify():唤醒对象监视器上等待中的一个线程
notifyAll():唤醒对象监视器上等待中的所有线程
上述的三个方法都必须在同步中使用
二、监视器方法演示
对于监视器方法在线程交互中的使用,看一个简单的例子
这是一个接收数据并打印的例子,一个线程接收数据,一个线程打印数据。要求输入一个数据,打印一个数据,于是要用到监视器方法。
public class Resource {
private String name;
private String sex;
private boolean flag = false;//标记表示输入状态,真为输入完毕
public synchronized void set(String name,String sex){
if(flag){//若标记为真,表明输入完毕,线程等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name;
this.sex = sex;
flag = true;
notify();//唤醒输出线程
}
public synchronized void out(){
if(!flag){//若标记为假,表明输出完毕,输出线程等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(name+"...+..."+sex);
flag = false;
notify();//唤醒输入线程
}
}
输入的线程,为了方便,循环进行两个数据的输入,达到不断输入数据的效果
public class Input implements Runnable {
Resource r;
public Input(Resource r) {
super();
this.r = r;
}
public void run() {
int x = 1;
while(true){
if(x==1){
r.set("旺财","公");
}else{
r.set("xiaoqiang","male");
}
x = (x+1)%2;
}
}
}
输出的线程
public class Output implements Runnable {
Resource r;
public Output(Resource r) {
super();
this.r = r;
}
public void run() {
while(true){
r.out();
}
}
}
部分执行结果如下
旺财...+...公
xiaoqiang...+...male
旺财...+...公
xiaoqiang...+...male
旺财...+...公
xiaoqiang...+...male
旺财...+...公
xiaoqiang...+...male
三、通过多生产者多消费者模型来了解交互问题
上面例子对与两个线程的交互进行了演示,而实际开发经常会碰到多个线程的并发协作。多个线程的并发协作比起上边的例子,有许多需要注意的地方,下面通过多线程的经典案例:多生产者多消费者模型来了解一下。
实际上,准确说应该是“生产者-消费者-仓储”模型,离开了仓储,生产者消费者模型就显得没有说服力了。
对于此模型,应该明确一下几点:
1、生产者仅仅在仓储未满时候生产,仓满则停止生产。
2、消费者仅仅在仓储有产品时候才能消费,仓空则等待。
3、当消费者发现仓储没产品可消费时候会通知生产者生产。
4、生产者在生产出可消费产品时候,应该通知等待的消费者去消费。
public class Resource {
private int count = 0;
public synchronized void produce(int n) {
while(count+n>=10){//wait()方法一定要用while循环判断
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count += n;
System.out.println(Thread.currentThread().getName()+"生产了"+n+"个馒头,"+"现在剩余"+count+"个");
notifyAll();//生产完毕后,唤醒全部线程,不能使用notify(),会造成死锁
}
public synchronized void consume(int n){
while(count-n<=0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count -= n;
System.out.println(Thread.currentThread().getName()+"消费了"+n+"个馒头,"+"现在剩余"+count+"个");
notifyAll();//消费完毕后,唤醒全部线程
}
}
public class Consumer implements Runnable {
Resource r;
int n;
public Consumer(Resource r,int n) {
super();
this.r = r;
this.n = n;
}
@Override
public void run() {
while(true){
r.consume(n);
}
}
}
public class Producer implements Runnable {
Resource r;
int n;
public Producer(Resource r,int n) {
super();
this.r = r;
this.n = n;
}
public void run() {
while(true){
r.produce(n);
}
}
}
public class Test {
public static void main(String[] args) {
Resource r = new Resource();
Producer pro1 = new Producer(r,2);
Producer pro2 = new Producer(r,3);
Consumer con1 = new Consumer(r,1);
Consumer con2 = new Consumer(r,1);
Thread tp1 = new Thread(pro1);
Thread tp2 = new Thread(pro2);
Thread tc1 = new Thread(con1);
Thread tc2 = new Thread(con2);
tp1.start();
tp2.start();
tc1.start();
tc2.start();
}
}
部分执行结果如下
Thread-3消费了1个馒头,现在剩余2个
Thread-3消费了1个馒头,现在剩余1个
Thread-1生产了3个馒头,现在剩余4个
Thread-1生产了3个馒头,现在剩余7个
Thread-0生产了2个馒头,现在剩余9个
Thread-2消费了1个馒头,现在剩余8个
Thread-2消费了1个馒头,现在剩余7个
Thread-2消费了1个馒头,现在剩余6个
这是一个简化版的多生产者多消费者模型的例子,通常仓储会使用1个数组来,这里省去了数组。
四、同步过程中的注意事项
根据上边的例子,可以看到有几点与前面的例子不同,来说一下原因
1.wait()方法一定要用循环判断
在多个线程的交互程序中,当线程从wait()方法引起的冻结状态中恢复过来后,数据的状态可能已经发生了变化,此时应再次对调用条件进行判断,以保证线程安全。
2.多个线程等待一个锁的时候使用notifyAll()
当多个线程等待一个锁的时候,若使用notify(),可能出现死锁现象。原因是有可能当消费者线程全部处于冻结状态时,生产者线程却唤醒了生产者线程,而此时有恰好不满足生产条件,就会出现所有的线程都冻结的死锁现象。
五、jdk5.0新特性对于同步的完善
1.锁-Lock
5.0以后将同步和锁封装成了对象,并将操作锁的隐式方法定义到了该对象中,将隐式动作变成了显式动作,方便了对锁和同步的操作。
2.条件变量-Condition
在5.0以后,Lock替代了synchronized方法和代码块,而Condition替代了监视器方法。
Condition通过指定Lock实例的newCondition()方法进行获取,它的await()、signal()、signalAll()方法对应监视器方法的wait()、notify()和notifyAll()方法。
synchronized方法和代码块只能绑定一组监视器方法,这就导致了在操作中的不方便,如上边例子中只能用notifyAll()而不能使用notify()。而一个Lock对象下可以绑定多组Condition对象。
我们可以把生产者和消费者分别用一个Condition对象来监视,这样方便了对不同类型线程的唤醒和冻结,提高了效率。
3.使用Lock和Condition的多生产者多消费者模型
接下来用Lock和Condition将多生产者多消费者的例子进行改写,了解一下它们的使用方法。
public class Resource {
private int count = 0;
//创建一个锁对象
Lock lock = new ReentrantLock();
//通过已有的锁获取该锁上的两组监视器,一组监视生产者,一组监视消费者
Condition producer_con = lock.newCondition();
Condition consumer_con = lock.newCondition();
public void produce(int n) throws InterruptedException{
lock.lock();
try{
while(count+n>=10){
try {
producer_con.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count += n;
System.out.println(Thread.currentThread().getName()+"生产了"+n+"个馒头,"+"现在剩余"+count+"个");
//生产者生产完毕后,唤醒消费者
consumer_con.signal();
}
finally{
lock.unlock();
}
}
public void consume(int n) throws InterruptedException{
lock.lock();
try{
while(count-n<=0){
try {
consumer_con.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count -= n;
System.out.println(Thread.currentThread().getName()+"消费了"+n+"个馒头,"+"现在剩余"+count+"个");
//消费者消费完毕后,唤醒生产者
producer_con.signal();
}
finally{
lock.unlock();
}
}
}
从例子中可以看出,Lock和Condition完美的替代了synchronized方法和监视器方法,并且提高了效率。
需要注意的是:Lock对象一定结束一定要调用unlock()方法,所以应按以下格式使用Lock:
Lock l = ...;
l.lock();
try {
...
} finally {
l.unlock();
}
总结
对于多线程的学习和理解暂时就这么多,更多的5.0的新特性在转载的博客中有介绍,我还没有全部消化,就先不写出来了。欢迎大家提出我文章中的问题,谢谢!