java 多线程学习总结
多线程技术大大提高了软件程序的效率,尤其是当程序需要与硬件交互时。现在的软件产品越来越重视用户体验,而软件产品的执行效率直接决定了用户体验的好坏。
对多线程的学习需要了解以下几个概念。
进程:
是一个正在执行的程序,每个进程都有执行顺序。该顺序是一个执行路径或一个控制单元。
线程:就是进程中的控制单元,线程控制着进程的执行。一个进程至少包含一个线程。
java开启线程的方式有两种。(准确的说java创建线程的的方式只有一种,就是创建 Thread类或其子类的对象。)
方式一:1
1.继承Thread类,复写其run()方法。将要开启线程执行的代码写入run()方法中。
2.调用start()方法开启线程。
具体代码如下:
//创建类,覆盖run方法
class MyThread extends Thread{
run(){
while(true){
System.out.println("Hello Thread");
}
}}
//启动线程
public static void main(String[]args){
MyThread my=new MyThread();
my.start();
}
第一种开启线程的思想是:根据java的面向对象思想。如果希望自己的类能成为一个线程并被JVM作为一个单独的线程独立执行的话,就应该加入该体系中,所以要继承Thread类。覆盖run方法是因为,run方法是用来存放线程要执行的代码,重写run方法就可以让我们创建的线程按我们要求的方式执行。
第二种方式:
1.定义类实现Runnable 接口。
2.覆盖run方法将需要运行的代码存放进去。
3.通过Thread类创建线程对象。
4.将实现Runnable接口的对象作为实际参数传入Thread的构造函数中。
5.调用Thread对象的start方法开启线程。
具体代码如下
//定义类实现Runnable接口,并复写run方法。
public class MyThread implements Runnable{
public void run(){
while true(){
System.out.println("Hello Thread")
}
}
}
//开启线程
public static void main(String[]args){
MyThread myt=new MyThread();
Thread t=new Thread (myt);
t.start();
}
第二种方式并不像第一种,我们要继承Thread类,创建一个线程。我们用第二中方式开启一个线程,只需要定义一个类实现Runnable 接口。让该类具备可以被作为一个独立的执行顺序,被线程运行的功能。把他的对象传入Thread的构造函数中。调用Thread对象的start()就可开启线程。
java 为什么要如此设计多线程的两种开启方式呢?
弄清这个问题,我们可以从这两种开启多线程方式的区别入手。
java两种多线程开启方式的比较:
1.如果一个类通过继承Thread类来开启线程,它就不能有别的父类。这就受到了java单继承的限制。而第二种方式,只需实现Runnable接口,仍然可以继承别的类,这可要灵活多了。
2.线程执行的代码不同,第一种方式代码是存在 Thread 类的run方法中。而第二种方法代码是存在实现Runnable接口类的run方法中。
3.从程序设计角度来说。如果我们的需求是创建一个多线程,并需要修改Thread类的一些基本方法时,就需要用第一种方式。而我们只是需要开启多线程时,只需要使用第二中即可。
所以,以后选择这两种方式,只需根据需求而定。不过大多时候我们的需求都只是需要开启线程。那么第二种开启线程方式是最常用的的。java也同样建议我们用第二种方式。
避免了单继承的局限性,可以使线程共享资源,在一定程度上可以说节省了内存。
线程的运行状态(生命周期)
wait()或
start() sleep(time)|
被创建状态----->运行状态----->冻结状态
stop( ) | <----
或run()中的代码执行完毕 | sleep时间到或
| notify()
消亡状态
上图还省略了每个线程都会处于的一种状态,临时状态。
临时状态是指每个线程在处于运行状态时,具有了执行权,但没有被CPU执行,既没有运行权,我们亦可以将其称为阻塞状态。(多线程同时运行的原理是CPU瞬时间在各个线程间来回切换运行,所以我们感觉上是各个线程同时运行。qq和酷狗音乐同时玩着。)
线程安全问题:
当多线程操作共享数据时,就会出现多线程安全问题。请看以下示例代码:
package com.heima.duoxiancheng;
/*
* 本文件将用代码方式模仿现实中车站多窗口卖票情景
* 并描述多线程安全问题产生原因,和解决方法。
*/
public class DuoXianChengAnQuan { public static void main(String[] args) {
// TODO Auto-generated method stub
TicketThread th=new TicketThread();
Thread t1=new Thread(th);
Thread t2=new Thread(th);
Thread t3=new Thread(th);
Thread t4=new Thread(th);
t1.start();
t2.start();
t3.start();
t4.start();
}}
class TicketThread implements Runnable {
int ticket = 1000;
Object obj=new Object();
public void run() {
while (true) {
synchronized (obj) {
/*synchronized同步语句块中是多条句
操作线程共享数据。会有发生多线程安全问题的可能。假设在ticket=1时,线程一经过if语句进入这里,由于cpu在切换到别的线程,它进入阻塞状态停在这里,现程二又进入了,然后线程一又被cpu执行,将ticket变为0,线程二执行,注意这时它就会将ticket变为-1.这就是对ticket的错误操作(ticket是大于0的)。这时synchronized语句块的作用就体现出来了。它将会将这些语句锁起来,在已有一个线程进来时,直到该线程执行完所有语句,把锁打开时,synchronized语句块才允许另一个线程进来。这就从而解决了多线程安全问题。
*/
if (ticket > 0) {
try {
Thread.sleep(10);//该方法是 为了让多线程安全问题更容易产生。
} catch (InterruptedException e) { e.printStackTrace();
}
ticket--;
System.out.println("窗口" + Thread.currentThread().getName()
+ "卖出第" + ticket + "张火车票");
}
}
} }
}
从以上代码中可以看出,多线程安全问题的原因是,有多条语句操作多线程共享数据,由于多线程在运行时CPU的随机切换线程执行,会导致一个线程没有执行完那些语句,另外的线程就又参与进来执行,导致数据处理的错误,引发了多线程安全问题。
多线程安全问题的的解决:
在知道多线程安全问题引发的原因,那解决他的原理也就一目了然了。
对多条线程共享数据的操作语句,要保证只能让一个线程执行完,再让另一个线程执行。在执行过程中不能让让别的线程参合进来。java中提供了专业的解决办法synchoized的语句块和synchoized函数。
格式如下
synchoized(对象名){
操作共享数据的代码
}synchoized void play(){
操作共享数据的代码
}
synchoized语句块保证了每个线程在执行操作共享数据语句时,不会有别的线程参与进来。当操作功效数据的语句时整个函数的语句时,可以将该函数定义为synchoized函数,可以起到同样的效果。
如何找到需要同步的语句?
1.明确多线程运行的代码。
2.明确多线程共享的数据
3.从多线程运行的代码中找出操作共享数据的语句即是需要同步的语句。
同步代码块中的锁。
同步代码块中。synchoized后的括号里要放置一个对象作为锁,同步代码块才能生效。
同步代码块中的锁可以是任意对象,但必须是线程间共享对象。
非静态同步函数的锁就是其对象的引用this,而静态同步函数的锁就是其类文件对象 类名.class
以下代码是对上面结论的验证,以期我们能更好的理解同步中锁的概念
package com.heima.duoxiancheng;
/*
* 本文件描述两个线程卖票,并让个线程再各自的代码中运行,以证明
* 同步函数的锁是this,和线程间同步必须用的是同一把锁。
* +=+额外所获:本例也表明了我们可以让多个线程运行不同的代码。
*/
public class DuoXianChengAnQuanWenTi2 { public static void main(String[] args) throws InterruptedException {
TicketThread1 th=new TicketThread1();
Thread t1=new Thread(th);
Thread t2=new Thread(th);
t1.start();
Thread.sleep(10);
th.flag=false;
t2.start();
}
}class TicketThread1 implements Runnable {
int ticket = 1000;
boolean flag=true;
Object obj=new Object();
public void run() {
if(flag){
while (true) {
//在下面同步語句塊中之前放的锁是obj,程序运结果有0号票
//明显这是多线程安全问题引起的。但将其改为this,问题就解决了。
//这表明,同步函数用的就是this作为锁,多线程只能被同一个对象作为锁
//才能被锁住。
synchronized (this) {
if (ticket > 0) {
try {
Thread.sleep(30);//该方法是为了让多线程安全问题更容易产生。
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("窗口" + Thread.currentThread().getName()
+ "卖出第" + ticket-- + "张火车票"+"run");
}
}
}
}else{
while(true){
run2();
}
} }
public synchronized void run2(){
if (ticket > 0) {
try {
Thread.sleep(30);//该方法是为了让多线程安全问题更容易产生。
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("窗口" + Thread.currentThread().getName()
+ "卖出第" + ticket-- + "张火车票"+"run2");
}
}
}
线程同步的前提:
从以上只是我们可以得出判断线程是否可以同步的两个前提。
1.是否是两个或两个以上线程。
2.线程间是否用的是同一把锁,即同一个对象。
多线程同步中的死锁情况。
当同步中嵌套同步时,线程间互相持有对方线程进入同语句的琐时,就会出现死锁的情况,在写程序时一定要避免该情况的发生。
下面代码展示死锁发生的情形及解决方法。
package com.heima.duoxiancheng;
/*
* 本文件展示多线程中的死锁情形并给出解决方案。
*/
public class DuoXianChengSiSuo { public static void main(String[] args) {
// TODO Auto-generated method stub
Xiancheng x=new Xiancheng();
Thread t1=new Thread(x);
Thread t2=new Thread(x);
t1.start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
x.flag=false;
t2.start();
}}
class Xiancheng implements Runnable {
boolean flag = true;//用于标记,让两个线程进入不同的代码区域执行。 public void run() {
if (flag) {
while (true) {
/*
* 下面代码就是同步中嵌套同步。
* 线程t1进入第一个同步中拿到了this锁
* 线程t2进入第一同步中拿到了Xiancheng.class
* 锁。当t1要进入第二个同步中时就需要 Xiancheng.class
* t2进入第二个同步中时需要this锁。这时就发生了
* 线程间互相持有对方线程进入同步语句的琐的情形,即出现死锁
*/
synchronized (this) {
synchronized (Xiancheng.class) {
System.out.println("线程一运行");
}
}
}
} else {
while (true) {
synchronized (Xiancheng.class) {
synchronized (this) {
System.out.println("线程二运行");
}
}
}
}
}}
对死锁情形的避免只需不让线程不互相持有对方进入同步的锁即可。
多线程通信问题。
当线程间对共享数据操作不是一样的时候,就会涉及到线程间通信问题。比如程序中线程对数据进行存与取操作时,负责存的线程就必须与负责取得数据进行通信才能保证不发生安全问题。这是关用同步的技术已经不足以高效的解决问题了,我们还必须要进行线程间通信,要遵循线程间等待唤醒机制。
何为等待唤醒机制?
就是一个线程在操作共享数据时,另外的线程进入冻结状态,直到该线程完成操作后再唤醒另外的线程进行对共享数据进行操作。
线程间通信为了便于学习理解,我们可以把它分为只有两条线程间的通信和两条以上多条线程间通信这两种情形。
以下代码展示只有两条线程间通信及等待唤醒机制的运用。
package com.heima.duoxiancheng;
/*
* 本文件演示多线程通信问题
* 具体是关于两个线程对一个资源类分别进行存取操作
* 步骤:1.找到需要同步的代码
* 2.控制线程协调工作,一个存数据时,另个就等待。一个取数据时另个就等待
* 并且每个线程在完成存或取动作时,要唤醒对方。
*
*/
public class DuoXianChengTongXin { public static void main(String[] args) {
// TODO Auto-generated method stub
Res r=new Res();
new Thread(new Input(r)).start();
new Thread(new OutPut(r)).start();
}
}
class Res{
String name;
String sex;
boolean flag=false;//每个线程是否完成工作的标记
}
class Input implements Runnable{
private Res r;
public Input(Res r) {
this.r=r;
}
@Override
public void run() {
boolean flag1=true;
while (true){
synchronized (r) {
if(!r.flag){
if(flag1){
r.name="mike";
r.sex="man man";
flag1=false;
}else{
r.name="乐乐";
r.sex="女 女";
flag1=true;
}
r.flag=true;
r.notify();
}else{
try {
r.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
}
class OutPut implements Runnable{
private Res r;
public OutPut(Res r) {
this.r=r;
}
public void run() {
while(true){
synchronized (r){
if(r.flag){
System.out.println(r.name+"....."+r.sex);
r.flag=false;
r.notify();
}
else{
try {
r.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
}
以下代码是演示多条线程间的通信。要注意的是这与只有两条线程间通信的一些小区别,掌握了这些区别也就掌握了多线程间的通信。
package com.heima.duoxiancheng;
/*
* 本代码是演示多线程通信更为普遍的现象。当对数据进行不
* 同操作(dui数据的操作总的操作其实就是两类,存储,提取)
* 不只是两条线程(一存一取),而是多条线程。
*/
public class ShengChanYuXiaoFei { public static void main(String[] args) {
// TODO Auto-generated method stub
ShangPin s = new ShangPin();
new Thread(new ShengChan(s)).start();
new Thread(new ShengChan(s)).start();
new Thread(new ShengChan(s)).start();
new Thread(new XiaoFei(s)).start();
new Thread(new XiaoFei(s)).start();
new Thread(new XiaoFei(s)).start();
}}
class ShangPin1 {
private String name;
private int NO=1;
private boolean flag = false; public synchronized void getName() {
while (!flag) {//同下面一样,防止线程对同个产品二次消费。
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("......."+Thread.currentThread().getName() + "消费者消费" + name
+ NO);
flag = false;
this.notifyAll();//这里将notify()变为NotifyAll是因为用While判断标记
//线程会都冻结,因为又一次判断标记,发现不满足就会睡去,并没执行唤醒代码。从而全部冻结
//而notifyAll保证了只有一个线程在冻结状态其他线程都会被唤醒。
} public synchronized void setName(String name) {
while (flag) {//这里有一个变革,以前是if。这是为了防止线程将本方线程唤醒
//本方线程会不在判断标记(flag)继续生产,将以前的生产在为消费前覆盖
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
this.name = name;
NO++;
System.out.println(Thread.currentThread().getName()+"生产者生产"+NO+"号电脑");
flag = true;
this.notifyAll();
}}
class ShengChan implements Runnable {
private ShangPin s; ShengChan(ShangPin s) {
this.s = s;
} @Override
public void run() {
while (true) {
s.setName("电脑");
} }
}
class XiaoFei implements Runnable {
private ShangPin s; XiaoFei(ShangPin s) {
this.s = s;
} @Override
public void run() {
while (true) {
s.getName();
} }
}
JDK5.0新特性
JDK5.0是java里程碑式的升级。这里主要讲下关于多线程中的升级。用下面代码演示下
package com.heima.duoxiancheng;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;/*
* 本文件展示了JDK1.5新的升级关于多线程之间的通信。
* 用Lock 代替了Synchorized.
object的wait().notify()等方法被 Condition的await().等方法取代。而且一个lock可以拥有
* 多个 Condition,这就方便了我们可以制定线程对他进行等待唤醒操作并将同步中的锁操作由隐式的变为显式操作。
*/
public class MyClock { public static void main(String[] args) {
// TODO Auto-generated method stub
ShangPin s = new ShangPin();
new Thread(new ShengChan(s)).start();
new Thread(new ShengChan(s)).start();
new Thread(new ShengChan(s)).start();
new Thread(new XiaoFei(s)).start();
new Thread(new XiaoFei(s)).start();
new Thread(new XiaoFei(s)).start();
}}
class ShangPin {
private String name;
private int NO = 1;
private boolean flag = false;
private ReentrantLock rlock = new ReentrantLock();
private Condition cond_sheng = rlock.newCondition();
private Condition cond_xiao = rlock.newCondition(); public void getName() {
rlock.lock();//同步中锁定操作
try {
while (!flag) {// 同下面一样,防止线程对同个产品二次消费。 cond_xiao.await();//明确的指出了让指定线程等待
}
System.out.println("......." + Thread.currentThread().getName()
+ "消费者消费" + name + NO);
flag = false;
cond_sheng.signal();//明确指定让指定线程唤醒 } catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
rlock.unlock();//释放锁资源操作一定要执行放在finally语句块中
}
} public void setName(String name) {
try {
rlock.lock();
while (flag) {
cond_sheng.await();
}
this.name = name;
NO++;
System.out.println(Thread.currentThread().getName() + "生产者生产" + NO
+ "号电脑");
flag = true;
cond_xiao.signal();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
rlock.unlock();
}
}}
如何中断现程?
中断线程的一般方法就是run方法结束。而run方法的结束只需要控制住循环即可。
因为我们开启线程执行的代码,通常都是循环结构的,只要控制住了循环结构,就能掌控线程的中断。
不过还有一种情况需要注意,当现程处于冻结状态时,会不再判断循环结构里的循环判断条件,上面的方法就不再适用于线程的中断。当线程没有指定的唤醒方式时(notify.sleep时间到)就需要强制唤醒线程,清除线程的冻结状态,已让它能继续运行判断循环标记。Thread类中的interrupt()方法可以达成这个效果。
开发中开启多线程的写法:
在开发中多线程的写法多采用匿名内部类这一简化写法。如下面的代码
package com.heima.duoxiancheng;
/*本类展示了开发中常见的开启多线程的简便写法。
*
*/
public class ThreadTingChangXieFa { public static void main(String[] args) {
// TODO Auto-generated method stub
//继承Thraed类的匿名内部类的简化写方法
new Thread(){
public void run(){
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+"...."+i);
}
}
}.start();
//实现Runnable接口的匿名内部类的简化写方法
Runnable r=new Runnable(){ @Override
public void run() {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+"...."+i);
}
}
};
new Thread(r).start();
}}
java多线程中库函数总结
Thread的相关方法:
run():该方法用于存放线程要运行的代码。
start():Thread类中的方法用于开启一个线程.
static currentThread()
返回对当前正在执行的线程对象的引用。返回值类型:Thread
String getName()
返回该线程的名称。
void interrupt()
强制中断线程。
void setDaemon(boolean on)
将该线程标记为守护线程或用户线程。 在开启一个线程前调用。当所有的运行线程都是守护线程时,Java虚拟机就会退出执行。
join()
等待该线程终止。
当线程A执行到线程B的join方法时,A线程就会释放cpu资源,处于冻结状态,直到B线程执行完毕才会醒来消除冻结状态。
该方法多用于临时插入一个执行线程。
void setPriority(int newPriority)
更改线程的优先级。
static void yield()
暂停当前正在执行的线程对象,并执行其他线程。
当某个线程执行到该方法就会释放cpu资源暂时进入临时状态。
Object 的相关方法:
void notify()
唤醒在拥有共同锁上等待的单个线程。
void notifyAll()
唤醒在拥有共同锁上等待的所有线程。
void wait()
在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
void wait(long timeout)
在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量前,导致当前线程等待。
void wait(long timeout, int nanos)
在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量前,导致当前线程等待
Lock 的相关方法
void lock()
锁定,用于需要同步的语句的上一句。
void unlock()
释放锁。 用于需要同步语句之后,通常放在finally语句块状。
Condition newCondition()
返回绑定到此 Lock 实例的新 Condition 实例。
Condition 的相关方法
void await()
造成当前线程在接到信号或被中断之前一直处于等待状态。
boolean await(long time, TimeUnit unit)
造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
void signal()
唤醒一个等待线程。
void signalAll()
唤醒所有等待线程。