多线程及异常
多线程
【什么是进程和什么是线程】
进程:正在执行的程序,其实就是应用程序在内存中的那片空间
线程:进程中的一个执行单元,负责进程中的程序的运行,一个进程中至少要有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
注意::多线程的使用可以合理使用cpu的资源,如果线程过多会导致降低性能。程序之所以能同时进行是CPU处理程序时通过快速切换完成的,在我们看来好像随机一样,它的频率是我们肉眼不能分辨出来的,所以看起来就像同时进行的一样。
总结:什么时候使用多线程技术呢?
当多部分代码需要同时执行时,就需要使用多线程技术。
多线程解决了当部分线程遇到了循环而导致在指定停留时间过长,
无法执行下面的程序的问题。
【创建线程】
第一种方式:
1.1 定义一个类继承Thread。
1.2 重写run方法。
1.3 创建子类对象,就是创建线程对象。
1.4 调用start方法,开启线程并让线程执行,同时还会告诉jvm去调用run方法。
如下程序:
package xdp.cn.itcast;
class SubThread extends Thread{
int x=100;
public void run(){
while(true){
System.out.println(Thread.currentThread().getName()+"run"+x);
x--;
Thread.yield();
}
}
}
public class Test2 {
public static void main(String args[]){
SubThread sub1=new SubThread();
SubThread sub2=new SubThread();
sub1.start();
sub2.start();
}
}
为什么要这样做::
因为Thread类是描述线程的类,具备线程应有的功能,所以我们只需要继承它就行了。
为什么不创建Thread类的对象::
因为Thread类中的run方法中没有我们自己要执行的任务代码,而我们要执行另一个线程,必须要调用Thread类的start方法来开启一个线程,start方法会调用run方法。
创建线程的目的是什么?是为了建立单独的执行路径,让多部分代码实现同时执行。也就是说线程创建并执行需要给定的代码(线程的任务)。对于之前所讲的主线程,它的任务定义在main函数中。自定义线程需要执行的任务都定义在run方法中。Thread类中的run方法内部的任务并不是我们所需要,只要重写这个run方法,既然Thread类已经定义了线程任务的位置,只要在位置中定义任务代码即可。所以进行了重写run方法动作。
第二种方式:
创建线程的第二种方式。实现Runnable接口。
1,定义类实现Runnable接口:避免了继承Thread类的单继承局限性。
2,覆盖接口中的run方法。将线程任务代码定义到run方法中。
3,创建Thread类的对象:只有创建Thread类的对象才可以创建线程。
4,将Runnable接口的子类对象作为参数传递给Thread类的构造函数。
因为线程已被封装到Runnable接口的run方法中,而这个run方法所属于Runnable接口的子类对象,所以将这个子类对象作为参数传递给Thread的构造函数,这样,线程对象创建时就可以明确要运行的线程的任务。
5,调用Thread类的start方法开启线程。
如下程序:
package xdp.cn.itcast;
class SubThread implements Runnable{
int x=100;
public void run(){
while(true){
System.out.println(Thread.currentThread().getName()+"run"+x);
x--;
Thread.yield();
}
}
}
public class Test2 {
public static void main(String args[]){
SubThread sub=new SubThread();
Thread sub1=new Thread(sub);
Thread sub2=new Thread(sub);
sub1.start();
sub2.start();
}
}
那么实现Runnable接口为什么要这样做呢?
因为Thread类也是实现Runnable接口的,在Thread类中构造函数源代码是这样的:
class Thread implements Runnable{
...............
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
/**
* Allocates a new {@code Thread} object. This constructor has the same
* effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread}
* {@code (null, target, gname)}, where {@code gname} is a newly generated
* name. Automatically generated names are of the form
* {@code "Thread-"+}<i>n</i>, where <i>n</i> is an integer.
*
* @param target
* the object whose {@code run} method is invoked when this thread
* is started. If {@code null}, this classes {@code run} method does
* nothing.
*/
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
...........
}
简化如下:
class Thread implements Runnable{
private Runnable target;
Thread(){}
Thread( Runnable target) {
this.target=target;
}
public void run(){
if(target!=null){
target.run();
}
}
public void start(){
run();
}
.......
}
}
就是说如果调用的是Thread带Runnable对象的构造函数时会调用Runnable子类对象的的run方法,run方法中是自己的现线程任务代码。
实现Runnable接口的好处:
第二种方式实现Runnable接口避免了单继承的局限性,所以较为常用。
实现Runnable接口的方式,更加的符合面向对象,线程分为两部分,一部分线程对象,一部分线程任务。
继承Thread类:线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,既是线程对象,有又有线程任务。
实现runnable接口:将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。
Runnable接口对线程对象和线程任务进行解耦。
补充:
多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。
进行方法的压栈和弹栈。当执行线程的任务结束了,线程自动在栈内存中释放了。
但是当所有的执行线程都结束了,那么进程就结束了。
【线程异常】
我们知道多线程程序在执行时是互不影响的,一个线程中断了,另一个线程会继续执行,这是多线程的一个特性:
package xdp.cn.itcast;
class SubThread extends Thread{
public void run(){
int[] arr=new int[]{1,2,3,4,5,7,8,23,9,77};
System.out.println(arr[13]);
for(int i=0;i<arr.length;i++){
System.out.println(Thread.currentThread().getName()+"....."+arr[i]);
}
Thread.yield();
}
}
public class Test2 {
public static void main(String args[]){
SubThread sub1=new SubThread();
SubThread sub2=new SubThread();
sub1.start();
sub2.start();
while(true){
System.out.println("main");
}
}
}
//output:
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 13
at xdp.cn.itcast.SubThread.run(Test2.java:9)
Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: 13
at xdp.cn.itcast.SubThread.run(Test2.java:9)
mainmainmainmainmainmainmainmainmainmainmainmainmainmainmainmainmainmainmainmain
这些运行异常伴随着throw的抛出异常结束功能,如果功能是run方法里的线程任务,那么线程就会结束。
而且异常信息中会体现出该异常在哪个线程上发生。
【线程状态】
线程运行是有多种状态的:
1.1 创建 new Thread类或者其子类对象。
1.2 运行 start(). 具备者CPU的执行资格和CPU的执行权。
1.3 冻结 释放了CPU的执行资格和CPU的执行权。比如执行到sleep(time) wait() 导致线程冻结。
1.4 临时阻塞状态: 具备者CPU的执行资格,不具备CPU的执行权。
1.5 消亡:线程结束。run方法结束。
【多线程的安全问题】
案例1:售票的例子:
售票的动作需要同时执行,所以使用多线程技术。
package xdp.cn.itcast;
class Ticket implements Runnable {
private int tickets = 100;
public void run() {
while(true){
if(tickets>0){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
}
System.out.println(tickets--);
}
}
}
}
public class Test2 {
public static void main(String args[]) {
Ticket sub = new Ticket();
Thread t1 = new Thread(sub);
Thread t2 = new Thread(sub);
t1.start();
t2.start();
}
}
//output:
...
1
0
结果中出现了0号票
问题产生的原因;
1,线程任务中在操作共享的数据:tickets
2,线程任务操作共享数据的代码有多条(运算有多个):
if(tickets>0){
System.out.println(tickets--);
}
当t1执行到if语句判断时,tickets=1>0,满足条件,将要执行System.out.println(tickets--)时线程t2获得了CPU执行权,打印tickets=1,tickets--,然后t1又获得了CPU执行权,打印tickets=0,出现问题。
解决思路:
只要让一个线程在执行线程任务时将多条操作共享数据的代码执行完,
在执行过程中,不要让其他线程参与运算,就可以了。
Java中解决此问题通过代码块来完成的。
这个代码块:同步代码块 synchronized
格式:
synchronized(对象)
{
//需要被同步的代码。
}
对象是任意对象。
public void run() {
while (true) {
synchronized (obj) {
if (tickets > 0) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
}
Thread.yield();
System.out.println(Thread.currentThread().getName() + "..."
+ tickets--);
}
}
}
}
同步好处:
解决多线程安全问题。
同步弊端:
降低了程序的性能。
同步前提:
必须保证多个线程在同步中使用的是同一个锁。
解决了什么问题?
当多线程安全问题发生时,加入了同步后,
问题依旧,就要通过这个同步的前提来判断同步是否写正确。
同步的另一种体现形式:同步函数。
同步函数使用的锁是哪个?
验证:
写一个同步代码块,写一个同步函数,如果同步代码块中的锁对象和同步函数中的锁对象是同一个,
就同步了,就没有错误的数据了。如果不是同一个锁对象,就不同步出现错误数据。
让两个线程,一个线程在同步代码块中执行,一个线程在同步函数中执行。
package xdp.cn.itcast;
class Ticket implements Runnable {
private int tickets = 100;
private Object obj = new Object();
boolean flag = true;
public void run() {
if (flag) {
while (true) {
synchronized (obj) {
if (tickets > 0) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
}
Thread.yield();
System.out.println(Thread.currentThread().getName()
+ "..." + tickets--);
}
}
}
} else {
while (true) {
this.sale();
}
}
}
private synchronized void sale() {
if (tickets > 0) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
}
Thread.yield();
System.out.println(Thread.currentThread().getName() + "..."
+ tickets--);
}
// TODO Auto-generated method stub
}
}
public class Test2 {
public static void main(String args[]) {
Ticket sub = new Ticket();
Thread t1 = new Thread(sub);
Thread t2 = new Thread(sub);
t1.start();
sub.flag = false;
t2.start();
}
}
数据结果没出现问题
所以同步函数使用的锁是this。
同步函数和同步代码块有什么区别吗?
同步函数使用的锁是固定的this。当线程任务只需要一个同步时完全可以使用同步函数。
同步代码块使用的锁可以是任意对象。当线程任务中需要多个同步时,必须通过锁来区分,这时必须使用同步代码块。同步代码块较为常用。
static 同步函数,使用的锁不是this,而是字节码文件对象, 类名.class。
【单例懒汉式被并发访问】
package xdp.cn.itcast;
class Single {
private static Single single = null;
private Single() {
}
public static Single getInstance() {
if (single == null)
single = new Single();
return single;
}
}
class Demo1 implements Runnable {
public void run() {
System.out.println(Single.getInstance().toString());
}
}
public class Test2 {
public static void main(String args[]) {
Demo1 d = new Demo1();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();
t2.start();
}
}
//output:
xdp.cn.itcast.Single@3bc257
xdp.cn.itcast.Single@785d65
发现结果出现了两个Single对象,出现了问题,
分析得知在
public static Single getInstance() {
if (single == null)
single = new Single();
return single;
}
时对同一个对象进行了并发访问,记性了多次操作运算!
解决:
可以在getInstance()上加上同步关键字synchronized:
public synchronized static Single getInstance() {
if (single == null)
single = new Single();
return single;
}
改进:
我们发现每次执行getInstance()时都要判断是否是同一把锁,但是效率会降低,所以可以使用同步代码块,加上双重对引用变量的判断。
改进如下:
public static Single getInstance() {
if(single==null){
synchronized(Single.class){
if (single == null)
single = new Single();
}
}
return single;
}
//Output:
xdp.cn.itcast.Single@811c88
xdp.cn.itcast.Single@811c88
注意这里的return语句是原子性的,不会产生同步问题。
【死锁】
死锁:线程都为结束,都处于不执行状态,这时就称为死锁。
常见场景:
1 同步嵌套。
2 都处于冻结状态。
注意:尽量避免死锁。
package xdp.cn.itcast;
class Ticket implements Runnable
{
private int tickets = 100;
private Object obj = new Object();
boolean flag = true;
public void run()
{
if(flag){
while(true){
synchronized(obj){
sale();//this lock;
}
}
}
else{
while(true){
this.sale();
}
}
}
public synchronized void sale()//this lock
{
synchronized(obj)//obj lock
{
if(tickets>0)
{
try{Thread.sleep(10);}catch(InterruptedException e){}
System.out.println(Thread.currentThread().getName()+"...sale..."+tickets--);//打印线程名称。
}
}
}
}
public class Test2
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try{Thread.sleep(10);}catch(InterruptedException e){}
//切换标记,之前,让主线程停一会,这时就只有一个t1线程在,它就会执行同步代码块。
t.flag = false;
t2.start();
}
}
【多线程间通信】
多线程间通信:多个线程执行任务不同,但是处理资源相同。
案例2:生产者消费者问题
实现:单生产单消费
package xdp.cn.itcast;
//描述同一个共享的资源
class Resources {
private String name;
private int id = 1;
public void product(String name) {
this.name = name + id;
id++;
System.out.println(Thread.currentThread().getName() + "protect."
+ this.name);
}
public void consume() {
System.out.println(Thread.currentThread().getName() + "consume."
+ this.name);
}
}
// 描述生产者
class Producter implements Runnable {
private Resources r;
public Producter(Resources r) {
this.r = r;
}
public void run() {
while (true) {
r.product("面包");
Thread.yield();
}
}
}
// 描述消费者
class Consumer implements Runnable {
private Resources r;
public Consumer(Resources r) {
this.r = r;
}
public void run() {
while (true) {
r.consume();
Thread.yield();
}
}
}
public class Test2 {
public static void main(String[] args) {
Resources r = new Resources();
Producter p = new Producter(r);
Consumer c = new Consumer(r);
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
我们发现数据错误:已经被生产很早期的商品,才被消费到或者商品还没生产就被消费到了。
出现线程安全问题,加入了同步解决。使用同步函数
//output:
Thread-0protect.面包21375
Thread-1consume.面包21375
Thread-0protect.面包21376
Thread-1consume.面包21376
Thread-1consume.面包21376
Thread-1consume.面包21376
Thread-1consume.面包21376
发现了连续生产却没有消费,同时对同一个商品进行多次消费。
希望的结果应该是生产一个商品,就被消费掉。生产下一个商品。
思路::
生产者生产了商品后应该告诉消费者来消费。这时的生产者应该处于等待状态。
消费者消费了商品后,应该告诉生产者,这时消费者处于等待状态。
等待:wait();
告诉:notify()、notifyAll();//唤醒
class Resources {
private String name;
private int id = 1;
private boolean flag=false;
public synchronized void product(String name) {//当前对象r调用
if(flag)
try {
this.wait();
} catch (InterruptedException e) {
}
this.name = name + id;
id++;
System.out.println(Thread.currentThread().getName() + "protect."
+ this.name);
transformFlag();
this.notify();
}
public synchronized void consume() {//当前对象r调用
if(!flag)
try {
this.wait();
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + "consume."
+ this.name);
transformFlag();
this.notify();
}
public void transformFlag(){
flag=!flag;
}
}
wait(): 会让线程处于等待状态,其实就是将线程临时存储到了线程池中。
notify():会唤醒线程池中任意一个等待的线程。
notifyAll():会唤醒线程池中所有的等待线程。
记住:这些方法必须使用在同步中,因为必须要标识wait,notify等方法所属的锁。
同一个锁上的notify,只能唤醒该锁上的被wait的线程。
补充:
为什么这些方法定义在Object类中呢?
因为这些方法必须标识所属的锁,而锁可以是任意对象,任意对象可以调用的方法必然时Object类中的方法。
实现:多生产多消费
被唤醒的线程没有判断标记
解决:只要让被唤醒的线程必须判断标记就可以了。将if判断标记的方式改为while判断标记。
记住:多生产多消费,必须是while判断条件。
问题2:发现while判断后,死锁了。
原因:因为notify()唤醒的任意性造成生产方唤醒了线程池中生产方的线程。本方唤醒了本方。
解决:希望本方要唤醒对方,没有对应的方法,所以只能唤醒所有。
class Resources {
private String name;
private int id = 1;
private boolean flag=false;
public synchronized void product(String name) {//当前对象r调用
while(flag)
try {
this.wait();
} catch (InterruptedException e) {
}
this.name = name + id;
id++;
System.out.println(Thread.currentThread().getName() + "protect."
+ this.name);
transformFlag();
this.notifyAll();
}
public synchronized void consume() {//当前对象r调用
while(!flag)
try {
this.wait();
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + "consume."
+ this.name);
transformFlag();
this.notifyAll();
}
public void transformFlag(){
flag=!flag;
}
}
问题解决了,但是发现在使用notifyAll()时没必要唤醒本方的线程,这使得程序效率低下。
【解决多生产多消费的效率问题】
1,使用JDK1.5后 java.util.concurrent.locks包中的对象。
2,Lock接口和ReentrantLock子类和Condition接口。
Lock接口:替代了同步函数或者同步代码块。将隐式锁操作变成了显示锁操作。
lock(); unlock();newCondition();
注意;unlock一定要定义在finally中。
Condition接口:替代了Object中的监视器方法。将这些监视器方法进行了单独的封装,封装到了Condition对象。也就是说Condition的出现将锁和监视器方法分离。await() signal() signalAll();最大的好处,以前必须是一个锁有一组监视器方法,而现在一个锁上可以多个有监视器对象。
class Resources {
private String name;
private int id = 1;
private boolean flag=false;
private final Lock lock=new ReentrantLock();
private Condition con=lock.newCondition();
private Condition pro=lock.newCondition();
public void product(String name) {
lock.lock();
try{
while(flag)
try {
pro.await();
} catch (InterruptedException e) {
}
this.name = name + id;
id++;
System.out.println(Thread.currentThread().getName() + "protect."
+ this.name);
transformFlag();
con.signal();
}finally{
lock.unlock();
}
}
public void consume() {
lock.lock();
try{
while(!flag)
try {
con.await();
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + "consume."
+ this.name);
transformFlag();
pro.signal();
}finally{
lock.unlock();
}
}
public void transformFlag(){
flag=!flag;
}
}
【多线程细节】
sleep和wait方法的异同点
相同点:可以让线程处于冻结状态(释放执行权和执行资格)
不同点:
1,
sleep必须指定时间。
wait可以指定时间,也可以不指定时间。
2,
sleep时间到,线程处于临时阻塞或者运行。
wait如果没有时间,必须要通过notify或者notifyAll唤醒。
3,
sleep不一定非要定义在同步中。
wait必须定义在同步中。
4,
如果都定义在同步中,
线程执行到sleep,不会释放锁。
线程执行到wait,会释放锁。
线程如何停止:
线程结束:就是让线程任务代码执行完,run方法结束。run方法中通常都定义循环,只要控制住循环就行了。
万一线程在任务中处于了冻结状态,那么它还能去判断标记吗?不能!
通过查阅stop方法的描述,发现提供了一个解决方法:
如果目标线程等待很长时间,则应使用 interrupt 方法来中断该等待,所谓的中断并不是停止线程。
interrupt的功能是 将线程的冻结状态清除,让线程恢复到的运行状态(让线程重新具备cpu的执行资格。
因为是强制性的所以会有异常InterruptedException发生,可以在catch中捕获异常,在异常处理中,改变标记让循环结束,让run方法结束。
package xdp.cn.itcast;
class StopThread1 implements Runnable {
private static boolean flag = true;
public void run() {
while (flag) {
try {
wait();// t1 t2
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().toString() + "....."
+ e.toString());
getFlag();
}
System.out.println(Thread.currentThread().getName() + "----->");
}
}
public static void getFlag() {
flag = false;
}
}
public class Test3 {
public static void main(String[] args) {
StopThread1 s = new StopThread1();
Thread t1 = new Thread(s);
t1.start();
int x = 0;
while (true) {
if (x++ == 50) {
StopThread1.getFlag();
t1.interrupt();
break;
} else {
System.out.println("main...x=" + x);
}
}
System.out.println("over");
}
}
补充:
线程的匿名内部类。【面试题】
new Thread(new Runnable()
{
public void run()
{
System.out.println("runnable run");
}
}){
public void run()
{
System.out.println("subthread run");//执行。
}
}.start();

浙公网安备 33010602011771号