黑马程序员--java基础复习之多线程及线程间通信

时间:2022-12-16 10:59:35
------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------
进程: 进程:是一个正在执行的程序 每个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元

线程:
线程就是进程中的一个独立的控制单元,线程在控制着进程的执行。一个进程中至少有一个线程。

多线程:  在java虚拟机启动的时候会有一个java.exe的执行程序,也就是一个进程。
 该进程中至少有一个线程负责java程序的执行。而且这个线程运行的代码存在于main方法中。该线程称之为主线程。
 JVM启动除了执行一个主线程,还有负责垃圾回收机制的线程。 像种在一个进程中有多个线程执行的方式,就叫做多线程。

多线程的好处:  多线程的出现能让程序产生同时运行效果。可以提高程序执行效率。

就我们常见的,在电脑上,即能在记事本上打字,同时也能听音乐。这个就是多线程的应用。又比如360安全卫士,当我们在清理垃圾的时候,同时也能扫描系统漏洞。这同事是多线程的应用有。
1、如何在自定义的代码中,自定义一个线程呢?
通过对API的查找,java已经提供了对线程这类事物的描述。就是Thread类。




创建线程的第一种方式:继承Thread类

步骤:
1、定义类继承Thread。
2、复写Thread类中的run 方法
目的:将自定义代码存储在run方法中,让线程运行。
3、调用线程的start方法。该方法有两个作用:启动线程,调用run方法

如下:
class Test extends Thread
{
//复写父类中的run方法
public void run()
{
for(int i=0;i<30;i++)
{
System.out.println(Thread.currentThread().getName()+" run .."+i);
}
}
}
class ThreadDemo
{
public static void main(String[] agrs)
{
Test t1=new Test();
t1.start();//开启线程,并调用run方法
Test t2=new Test();
t2.start();
for(int i=0;i<60;i++)
{
System.out.println("main run::"+i);
}


}
}

运行多次后,发现运行结果每一次都不同。因为多个线程都在获取CPU的执行权。CPU执行到谁,谁就运行。
明确一点,在某一个时刻,只能有一个程序在运行。(多核除外)
cpu在做着快速的切换,以达到看上去是同时运行的效果。我们可以形象地把多线程的运行行为看作在互相抢夺CPU的执行权。

这就是多线程的一个特性:随机性。轮到谁,谁执行。

为什么要覆盖run方法呢?
Thread 类用于描述线程,该类就定义了一个功能,用于存储线程要运行的代码,该存储功能就是run方法。
也就是说Thread类中的run 方法,用于存储线程要运行的代码。
我们要利用线程运行我们自己的代码,所以我们也要将代码存放在我们自己的run方法中,所以可以直接调用父类中的run方法,并将其复写。

线程都有自己默认的名称。
Thread-编号  该编号从0开始

static Thread currentThread():获取当前线程对象
getName():获取线程名称
设置线程名称:setName或者构造函数


创建线程的第二种方式:实现 Runnable接口
步骤:
1、定义类实现Runnable接口
2、覆盖Runnable接口中的run方法。 将线程要运行的代码存放在run方法中 3、通过Thread类建立线程对象。 4、将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。 为什么要将Runnable接口的子类对象传递给Thread的构造函数。 因为,自定义的run方法所属的对象是Runnable接口 的子类对象。 所以要让线程去执行指定对象的run方法。 就必须明确该run方法所属对象。 5、调用Thread类的start方法开户线程并调用Runnable接口子类的Run方法

下面用一个卖票的小程序来说明:
//卖票线程
class Ticket implements Runnable
{
int num=100;
Object obj=new Object();
public void run()
{
while(true)
{
if(num>0)
{
System.out.println(Thread.currentThread().getName()+"Sale:"+num--);
}

}
}


}

class ThreadTest
{
public static void main(String[] args)
{
Ticket t=new Ticket();//创建一个对象,然后再开始四个线程,模拟四个窗口卖票
Thread th1=new Thread(t);//这里是用到了多态 ,传递的是Runnable类型或其子类的对象
Thread th2=new Thread(t);
Thread th3=new Thread(t);
Thread th4=new Thread(t);
th1.start();
th2.start();
th3.start();
th4.start();
}
}



通过实现Runnable接口方式创建线程的好处:避免了单继承的局限性。(当一个类本身就有一个父类,则不能再继承Thread类)
在定义线程时,建议使用实现方式。
区别:
继承Thread:线程代码存放在Thread子类的run方法中。
实现Runnable,线程代码存放在接口的子类的run方法中。

多线程中的安全问题
还是那个卖票程序,再看代码:
<span style="font-size:14px;">//卖票线程
class Ticket implements Runnable
{
int num=100;
Object obj=new Object();
public void run()
{
while(true)
{
if(num>0)
{
//在此处sleep() 10毫秒,模拟CPU运行到此处的时候,将执行权交给其他进程
try{Thread.sleep(10);} catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"Sale:"+num--);
}
}
}
}

class ThreadTest
{
public static void main(String[] args)
{
Ticket t=new Ticket();
Thread th1=new Thread(t);
Thread th2=new Thread(t);
Thread th3=new Thread(t);
Thread th4=new Thread(t);
th1.start();
th2.start();
th3.start();
th4.start();
}
}</span>
结果:黑马程序员--java基础复习之多线程及线程间通信    从结果中可以看到,票号出现了负数和0,这明显是不符合常理的。
造成这情况,是因为多线程的运行出现了安全问题
问题原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没执行完,另一个线程参与进来执
行,导致共享数据的错误。
解决办法:对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行。

1、java 对于多线程的安全问题提供了专业的解决方式,就是同步代码块
synchronized(对象)
{
需要被同步的代码
}

对象如同锁。持有锁的线程可以在同步中执行。没有持有锁的线程即使获取了CPU的执行权,也进不去,因为没有锁。

同步的前提:
1、必须要有两个或两个以上的线程
2、必须是多个线程使用同一个锁
必须保证同步中至少有一个线程在运行。


好处:
解决了多线程的安全问题
弊端:多个线程都需要判断锁,较为消耗资源。

同步代码块的使用,如下:
class Ticket implements Runnable
{
int num=100;
Object obj=new Object();
public void run()
{
while(true)
{

//同步
synchronized(obj)
{
if(num>0)
{
try{Thread.sleep(10);} catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"Sale:"+num--);
}
}

}
}

}

如上,同步代码块,关键字synchronized,这里的锁为任一对象。
2、解决线程安全问题的第二种方式。同步函数
格式:在函数上加上synchronized修饰符
那么,同步函数用的是哪一个锁呢? 函数需要被对象调用。那么函数都有一个所属对象引用。就是this。所以同步函数使用的锁是this
如上面的代码片段可以直接用同步代码块的方式完成,如下:
//卖票线程
class Ticket implements Runnable
{
int num=100;
Object obj=new Object();
public void run()
{
while(true)
{
this.Sale();
/*
//同步代码块
synchronized(obj)
{
if(num>0)
{
try{Thread.sleep(10);} catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"Sale:"+num--);
}
}
*/
}
}

public synchronized void Sale()
{
if(num>0)
{
try{Thread.sleep(10);} catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"Sale:"+num--);
}
}
}

那么问题来了,如何确认哪些代码需要被同步呢?如何寻找多线程中的安全问题呢?         a,明确哪些代码是多线程运行代码。         b,明确共享数据。
        c,明确多线程运行代码中哪些语句是操作共享数据的。


静态同步函数
如果同步函数被静态修饰后,使用的锁是什么呢?
通过验证,发现不再是this。因为静态方法中也不可以定义this。

静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象。
类名.class   该对象的类型是 Class

验证静态同步函数的锁为 类名.class
静态的同步函数,使用的锁是该方法所在类的字节码文件对象。 类名.class




说到多线程的安全问题,想到之前的单例设计模式。 单例设计模式的特点就是对象的唯一性。当多线程访问单例设计模式中的懒汉式的时候。因为懒汉式中有一个判断:if(s==null) s=new Single(); 这里是两行代码,当多线程运行到第一句发生CPU转移执行权的时候,会发生安全问题,这样,对象就可能不是一个,而是多个了,所以根据多线程的安全问题,我们可以对懒汉式进行同步操作,以解决懒汉式中的安全问题。如下:
class Single
{
// 构造函数私有化,就不能创建类的对象
private Single(){}
private static Single s=null;
//每次都要判断锁,比较低效
public static synchronized Single getInstance()
{
if(s==null)
s=new Single();
return s;
}
}
但是,每次线程访问getInstance()方法时,都有一个判断锁的动作,这样做效率比较低。所以又用同步代码块对懒汉式进行了优化:
class Single{// 构造函数私有化,就不能创建类的对象private Single(){}private static Single s=null;public static Single getInstance(){//使用双重判断if(s==null){//锁为该类所属字节码文件对象synchronized(Single.class){if(s==null)s=new Single();return s;}}}}
这里使用了双重判断,以免每次访问该函数时,在函数中要进行判断锁的操作,影响效率,使后面的访问只需要判断s是否为空即可。


死锁 死锁是什么?死锁即是同步中嵌套同步。如下例:
class Test implements Runnable
{
boolean flag;
Test(boolean flag)
{
this.flag=flag;
}
public void run()
{
if(flag)
{
synchronized(MyLock.obja)
{
System.out.println(" if obja");
synchronized(MyLock.objb)
{
System.out.println(" if objb");
}
}
}
else
{
synchronized(MyLock.objb)
{
System.out.println(" else objb");
synchronized(MyLock.obja)
{
System.out.println(" else obja");
}
}
}
}
}

class MyLock
{
static Object obja=new Object();
static Object objb=new Object();
}

class DeadLockTest
{
public static void main(String[] args)
{
Test te1=new Test(true);
Test te2=new Test(false);

Thread t1=new Thread(te1);
Thread t2=new Thread(te2);
t1.start();
t2.start();
}
}
结果:黑马程序员--java基础复习之多线程及线程间通信




线程间通信
线程间通信,即是多个线程操作同一资源,但操作的内容不同。 举例,A和B去银行办个人业务,其中A是存钱,B是取钱。操作的都是银行的金库。
如下例,两个线程操作一个共同资源,一个向资源内赋值,一个从资源中取值。希望两个线程交互,存一个,取一个
<span style="font-size:14px;">//共同资源
class Res
{
boolean flag=false;
String name;
String sex;
}

//输入类,实现Runnable接口
class Input implements Runnable
{
Res r;
Input(Res r)
{
this.r=r;
}
public void run()
{
int x=0;
while(true)
{
synchronized(r)
{
if(r.flag==false)
{
if(x==0)
{
r.name="zhangsa";
r.sex="man";
}
else
{
r.name="李四";
r.sex="女";
}
r.flag=true;
}


}
x=(x+1)%2;

}
}
}

//输出类
class Output implements Runnable
{
Res r;
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;
}

}

}
}
}

class InputOutputDemo
{
public static void main(String[] args)
{
//创建一个资源对象
Res r=new Res();

Input in=new Input(r);
Output out=new Output(r);

Thread t1=new Thread(in);
Thread t2=new Thread(out);

t1.start();
t2.start();

}
}</span>

结果:黑马程序员--java基础复习之多线程及线程间通信


上述代码使用线程等待唤醒机制来优化:
//资源
class Res
{
boolean flag=false; //标识,用于判断当前线程是应做存取操作还是等待
private String name;
private String sex;

//存入操作
public synchronized void set(String name,String sex)
{
if(flag) //若已存入资源,则线程等待
try{this.wait();}catch(Exception e){} //线程等待。
this.name=name;
this.sex=sex;
this.flag=true;
//唤醒线程池中的其他等待线程
this.notify();
}
//取出操作
public synchronized void out()
{
if(!flag)
try{this.wait();} catch(Exception e){}
System.out.println(name+":::"+sex);
this.flag=false;
this.notify();
}
}

//输入类,实现Runnable接口
class Input implements Runnable
{
Res r;
Input(Res r)
{
this.r=r;
}
public void run()
{
int x=0;
while(true)
{

if(x==0)
r.set("zhangsan","man");
else
r.set("李四","女");
x=(x+1)%2;


}
}
}

//输出类
class Output implements Runnable
{
private Res r;
Output(Res r)
{
this.r=r;
}
public void run()
{
while(true)
{
r.out();
}
}
}

class InputOutputDemo1
{
public static void main(String[] args)
{
//创建一个资源对象
Res r=new Res();

new Thread(new Input(r)).start();
new Thread(new Output(r)).start();

}
}



wait(): notify()
notifyAll();
都使用在同步中,因为要对持有监视器(锁)的线程操作。所以要使用在同步中,因为只有同步才具有锁

为什么这些操作线程的方法要定义在Objcect类中呢?
1、因为这些方法在操作同步中线程时,都必须要标识它们所操作线程持有的锁。
只有同一个锁上的被等待线程,可以被同一个锁上的notify唤醒,不可以对不同锁中的线程唤醒。也就是说,等待和唤醒必须是同一个锁。
2、锁可以是任意对象,所以可以被任意对象调用的方法定义在Object类中



JDK1.5中提供了多线程升级解决方法
将同步synchronized替换成显式的Lock操作。
将Object中的wait,notify  notifyAll,替换成Condition对象。 该对象可以通过Lock锁进行获取.lock.newConditiom

通过下面一个代码来实现,看看与synchronized有什么不同:
import java.util.concurrent.locks.*;
class Res
{
private String name;
private int num=1;
boolean flag=false;
//创建Lock接口的子接口,父类引用指向子类对象
private Lock lock=new ReentrantLock();
//创建Condition对象,用来控制生产者的锁,等待和唤醒本锁上的线程
private Condition condition_pro =lock.newCondition();
//创建Condition对象,用来控制消费者的锁,等待和唤醒本锁上的线程
private Condition condition_con =lock.newCondition();
public void set(String name) throws InterruptedException
{
lock.lock();

//因为使用Conditiom的await方法,会抛出异常
try
{
while(flag)
condition_pro.await();//线程等待,抛出异常
this.name=name+(num++);
System.out.println("生产者::::::"+this.name);
condition_con.signal();//消费者线程唤醒
flag=true;
}
//一定执行的操作:解开锁
finally
{
lock.unlock();
}


}

public synchronized void out()throws InterruptedException
{
//锁上锁
lock.lock();
try
{
while(!flag)
condition_con.await(); //消费者线程等待
System.out.println("消费者: "+name);
condition_pro.signal();//生产者线程唤醒
flag=false;
}
finally{
lock.unlock(); //解开锁
}

}
}

class Producter implements Runnable
{
Res r;
Producter(Res r)
{
this.r=r;
}
public void run()
{
while(true)
{
try
{
r.set("商品");
}
catch(InterruptedException e)
{

}

}
}
}
class Consumer implements Runnable
{
Res r;
Consumer(Res r)
{
this.r=r;
}
public void run()
{
while(true)
try
{
r.out();
}
catch(InterruptedException e)
{}

}
}

class ProConLockTest
{
public static void main(String[] args)
{
Res r=new Res();

Producter pro=new Producter(r);
Consumer con=new Consumer(r);

Thread t1=new Thread(pro);
Thread t2=new Thread(con);
Thread t3=new Thread(pro);
Thread t4=new Thread(con);

t1.start();
t2.start();
t3.start();
t4.start();
}

}

结果:黑马程序员--java基础复习之多线程及线程间通信

停止线程
两种方式: 1、定义循环结束标记  因为线程运行代码一般都是循环,只要控制了循环即可。如下:
class StopThread implements Runnable
{
private boolean flag=true;
public void run()
{
while(flag)
{
System.out.println("aaaaa");
}
}
//改变标志的值
public void chageFlag()
{
flag=false;
}
}

class StopThreadDemo
{
public static void main(String[] args)
{
StopThread st=new StopThread();

Thread t1=new Thread(st);
t1.start();

int num=0;
while(true)
{
if(num++==60)
{
st.chageFlag();
break;
}
System.out.println("num="+num);
}
}
}

特殊情况:
当线程处于了冻结状态,就不会读取到标记,那么线程就不会结束。当没有指定的方式让冻结的线程恢复到运行状态时,这时需要对冻结状态进行清除。强制让线程恢复到运行状态中来。这样就可以操作标记让线程结束。


2、使用interrupt(中断)方法 (stop方法已过时)
该方法中结束线程的冻结状态,使线程回到运行状态中来。
//Thread 类提供该方法 interrupt();
*/

class StopThread implements Runnable
{
private boolean flag=true;
public void run()
{
while(flag)
{
try
{
wait();
}
catch(InterruptedException e)
{
flag=false;
}
System.out.println(Thread.currentThread().getName()+"....");
}
}
}

class StopThreadDemo
{
public static void main(String[] args)
{
StopThread st=new StopThread();

Thread t1=new Thread(st);
t1.start();

Thread t2=new Thread(st);
t2.start();

int num=0;
while(true)
{
if(num++==60)
{
t1.interrupt();
t2.interrupt();
break;
}
System.out.println("num="+num);
}
System.out.println("over");
}
}


多线程什么时候用? 当某些代码需要同时被执行时,就用单独的线程进行封装。
如下:
class UseMulThread
{
public static void main(String[] args)
{
/*使用继承方式(匿名内部类)*/

new Thread(){
public void run()
{
for(int i=0;i<50;i++)
{
System.out.println("我是线程一");
}
}
}.start();


/******使用实现Runnable方式(匿名内部类)*********/

Runnable ra= new Runnable()
{
public void run()
{
for(int j=0;j<40;j++)
{
System.out.println("我是线程二");
}
}
};

new Thread(ra).start();


/***************主线程*********************/
for(int k=0;k<40;k++)
{
System.out.println("我是主线程");
}

}
}



join方法
        当A线程执行到了b线程的.join()方法时,A线程就会等待,等B线程都执行完,A线程才会执行。(此时A和其他线程交替运行。)join可以用来临时加入线程执行。


  setPriority()方法用来设置优先级
        MAX_PRIORITY 最高优先级10
        MIN_PRIORITY   最低优先级1
        NORM_PRIORITY 分配给线程的默认优先级5
优先级高是获取CPU概率大一些,优先级高并不一定就先执行。 yield()方法可以暂停当前线程,让其他线程执行。