一、理解多线程相关的概念
进程:是一个正在执行中的程序。每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元。
线程:就是进程中的一个独立的控制单元。线程在控制着进程的执行。
二者关系:一个进程中至少有一个线程。线程是进程中的内容。
Java VM 启动的时候会有一个进程java.exe.该进程中至少一个线程负责java程序的执行。而且这个线程运行的代码存在于main方法中。该线程称之为主线程。
扩展:其实更细节说明jvm,jvm启动不止一个线程,还有负责垃圾回收机制的线程。
二、线程的状态
三、自定线程
1、第一种方式,继承Thread类。
通过对api的查找,java已经提供了对线程这类事物的描述。就是Thread类。创建线程的第一种方式:继承Thread类。
(1)创建步骤:
1,定义类继承Thread。
2,复写Thread类中的run方法。
目的:将自定义代码存储在run方法。让线程运行。
3,调用线程的start方法,
该方法两个作用:启动线程,调用run方法。
//自定义线程
class Trd extends Thread{//创建一个线程
public void run(){//覆盖run方法
for(int i=0;i<50;i++)
System.out.println("---------------run"+i);
}
}
class T1 {
public static void main(String[] args) {
Trd t=new Trd();
Trd t1=new Trd();
Trd t2=new Trd();
t.start();//开启线程,此时它与主线程并存
t1.start();
for(int i=0;i<50;i++)
System.out.println("-------main------"+i);
t2.run();//只是调用函数方法,虽然建立了线程,但是没有开启
}}
上述代码发现运行结果每一次都不同。因为多个线程都获取cpu的执行权。cpu执行到谁,谁就运行。明确一点,在某一个时刻,只能有一个程序在运行。(多核除外)cpu在做着快速的切换,以达到看上去是同时运行的效果。我们可以形象把多线程的运行行为在互相抢夺cpu的执行权。这就是多线程的一个特性:随机性。谁抢到谁执行,至于执行多长,cpu说的算。
为什么重写run方法:
Thread类用于描述线程。该类就定义了一个功能,用于存储线程要运行的代码。该存储功能就是run方法。也就是说Thread类中的run方法,用于存储线程要运行的代码。
Thread t=new Thread();t就是一个线程,但是没有意义,因为它的run方法是Thread类的方法。
(2)Thread类中的几个方法
线程都有自己默认的名字,Thread-编号(编号从0开始)
Static Thread currentThread():获取当前线程对象
getName():获取线程名称
setName() 或者 构造函数来设置线程名称
class Trd1 extends Thread{
Trd1(String d){
super(d);//调用父类构造函数,给线程起名字
}
Trd1(){
}
public void run(){
for(int i=0;i<30;i++){
System.out.println(Thread.currentThread()+"+++++++++"+this.getName()+"----"+i);
}
}}
public class T2 {
public static void main(String[] args) {
Trd1 t0=new Trd1("哈哈哈哈哈");
Trd1 t1=new Trd1("哈哈哈哈哈");
Trd1 t2=new Trd1();
Trd1 t3=new Trd1();
t1.setName("hehehehhehehehehehhehehe");//t1重命名
//开启线程
t0.start();
t1.start();
t2.start();
t3.start();
for(int i=0;i<30;i++){
System.out.println("main"+i+Thread.currentThread());
}
t2.setName("222222222");
}
}
//简单的卖票
class Trd3 extends Thread{
private static int num=100;//为了让多个线程共用num,所以用静态
public void run(){
/*for(;num>=1;num--)
{
System.out.println(this.getName()+"剩余---"+(num-1));
}
这种方式会出现俩99或者三个99的原因就是1个线程执行到打印语句时,2线程在
打印语句之前等待,1线程打印完还没执行num--,2线程就抢到CPU执行打印语句
所以会出现重复
*/
while(num>0){
System.out.println(this.getName()+"剩余---"+(--num));
}//这种方式不会重复,但可能会出现负数
}
}
public class T3 {
public static void main(String[] args) {
Trd3 t1=new Trd3();
Trd3 t2=new Trd3();
Trd3 t3=new Trd3();
t1.start();
t2.start();
t3.start();
}
}
2、创建线程第二种方式,实现Runable接口
步骤:
1,定义类实现Runnable接口
2,覆盖Runnable接口中的run方法。
将线程要运行的代码存放在该run方法中。
3,通过Thread类建立线程对象。
4,将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
为什么要将Runnable接口的子类对象传递给Thread的构造函数。
因为,自定义的run方法所属的对象是Runnable接口的子类对象。
所以要让线程去指定指定对象的run方法。就必须明确该run方法所属对象。
5,调用Thread类的start方法开启线程并调用Runnable接口子类的run方法。
实现方式和继承方式有什么区别呢?
实现方式好处:避免了单继承的局限性。
在定义线程时,建立使用实现方式。
两种方式区别:
继承Thread:线程代码存放Thread子类run方法中。
实现Runnable,线程代码存在接口的子类的run方法。
class Trd4 implements Runnable{
private int num=100;
public void run(){
while(num>0){
/*这种方式会出现负数,原因就是1个线程执行到打印语句时,2线程在
打印语句之前等待,假设此时的num=0.1线程打印完,2线程就抢到CPU执行打印语句
此时出现负数*/
try{
Thread.sleep(10);}//使当前线程冻结10ms,从而会有多于1个线程在此停留
//以此来证明会出现负数
catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"线程在执行,剩余@@@@"+(--num));
}
}
}
public class T4 {
public static void main(String[] args) {
Trd4 t=new Trd4();
//开启了六个线程
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
上述代码可能会出现负数,多线程的运行出现了安全问题。
问题的原因:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,
另一个线程参与进来执行。导致共享数据的错误。
解决办法:
对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行。
Java对于多线程的安全问题提供了专业的解决方式。就是同步代码块。
synchronized(对象)(这个对象无要求)
{
// 需要被同步的代码
}
对象如同锁。持有锁的线程可以在同步中执行。没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。
火车上的卫生间---经典的同步的例子。或者把锁看成大门,每个线程都会随手关门,所以,进门就锁门,出去就开门。只要保证只有一个线程在门内。
同步的前提:
1,必须要有两个或者两个以上的线程。
2,必须是多个线程使用同一个锁。
必须保证同步中只能有一个线程在运行。
好处:解决了多线程的安全问题。
弊端:多个线程需要判断锁,较为消耗资源,
如何找问题:
1,明确哪些代码是多线程运行代码。
2,明确共享数据。
3,明确多线程运行代码中哪些语句是操作共享数据的。
(分开操作共享数据的语句之间,可以加sleep语句来进行测试。)
class Trd5 implements Runnable{
private int num=100;
private boolean t=true;
public void run(){
while(t){
synchronized("d"){//同步代码快,解决负数问题,明显感觉代码执行速度慢了许多
if(num>0){
try{
Thread.sleep(10);}
catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"线程在执行,剩余@@@@"+(--num));
}}
if(num==0)
t=false;
}
/*synchronized("d"){
while(num>0){
try{
Thread.sleep(10);}
catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"线程在执行,剩余@@@@"+(--num));
}}
这种方式显然不合适,因为只有一个线程执行while语句,就跟单线程一样*/
}
}
public class T5 {
public static void main(String[] args) {
Trd5 t=new Trd5();
//开启了六个线程
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}}
//金库
class Bank{
private int sum;
/*public void add(int i){
synchronized("dd"){//同步代码快
sum+=i;
try{Thread.sleep(10);}
catch(Exception s){}
System.out.println(sum);}
}*/
public synchronized void add(int i){//同步函数
sum+=i;
try{Thread.sleep(10);}
catch(Exception s){}
System.out.println(sum);
}
}
class P implements Runnable{
private Bank k=new Bank();
public void run(){
for(int i=0;i<3;i++){
k.add(100);
}
}
}
public class T6 {
public static void main(String[] args) {
P a=new P();
new Thread(a).start();
new Thread(a).start();
}
}
将synchronized放在函数上,就是同步函数。所以同步分两种形式:同步代码快和同步函数
//卖票
class Trd7 implements Runnable{
private int num=10;
private boolean t=true;
public void run(){
while(t){
show();
}
}
public synchronized void show(){//将同步代码快中的代码封装成函数,然后函数用synchronized修饰
if(num>0){
try{
Thread.sleep(10);}
catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"线程在执行,剩余@@@@"+(--num));
if(num==0)
t=false;}}
}
public class T7 {
public static void main(String[] args) {
Trd7 t=new Trd7();
//开启了六个线程
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}}
函数需要被对象调用。那么函数都有一个所属对象引用。就是this。
所以同步函数使用的锁是this。如果同步函数被静态修饰后,使用的锁是什么呢?通过验证,发现不在是this。因为静态方法中也不可以定义this。静态进内存是,内存中没有 本类对象,但是一定有该类对应的字节码文件对象 类名.class 该对象的类型是Class静态的同步方法,使用的锁是该方法所在类的字节码文件对象。 类名.class
对上述进行验证:
//验证同步函数的锁
class Trd8 implements Runnable{
private static int num=200;
private static boolean t=true;
boolean b=true;
public void run(){
if(b){
while(t){
synchronized(this){//只有锁是this时,代码运行正常。当show方法为static时,只有锁为Trd8.class代码运行正常(T8.class也不正常)
if(num>0){
try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"线程在执行,剩余@@@@"+(--num));
if(num==0)
t=false;
}}
}}
else
while(t){
show();
}
}
public synchronized void show(){
if(num>0){
try{
Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"-----show"+(--num));
if(num==0)
t=false;}}
}
class T8 {
public static void main(String[] args)throws Exception {
System.gc();
Trd8 t=new Trd8();
new Thread(t).start();
Thread.sleep(20);//让主线程睡眠10ms,目的就是让第一个线程运行,以防止b=false后两个线程都执行同步函数
t.b=false;
new Thread(t).start();
}}
死锁:
同步中嵌套同步。程序中应避免死锁出现。
//死锁
class Trd9 implements Runnable{
private boolean b;
Trd9(boolean b){
this.b=b;
}
public void run(){
if(b){
synchronized(cl.o1){
System.out.println("true---o1");
synchronized(cl.o2){
System.out.println("true---o2");
}
}
}
else{
synchronized(cl.o2){
System.out.println("false---o2");
synchronized(cl.o1){
System.out.println("false---o1");
}
}
}
}
}
class cl{
static Object o1=new Object();
static Object o2=new Object();
}
public class T9 {
public static void main(String[] args) {
new Thread(new Trd9(true)).start();
new Thread(new Trd9(false)).start();
}}
五、线程间通信
其实就是多个线程操作同一个资源,但是操作的动作不同
public class T10
{
public static void main(String[] args)
{
Zi z=new Zi();
new Thread(new In(z)).start();
new Thread(new Out(z)).start();
}
}
//描述被操作的资源
class Zi{
String name;
String sex;
}
class In implements Runnable
{
private Zi z;
In(Zi z)//这只是为了保证能够共用同一个资源
{
this.z=z;
}
public void run()
{
int x=0;
while(true)
{
synchronized(z)
{
if(x==0)
{
z.name="lisi";
z.sex="nan";
}
else
{
z.name="丽丽丽丽丽丽丽丽";
z.sex="女女女女女";
}
x=(x+1)%2;
}
}
}
}
class Out implements Runnable
{
private Zi z;
Out(Zi z)
{
this.z=z;
}
public void run()
{
while(true)
{
synchronized(z)
{
System.out.println(z.name+"-----"+z.sex);
}
}
}
}
/*解析:首先同步前,创建俩线程发现打印结果有错误,那就考虑同步,当对In中的run方法内的操作
* 资源的语句同步后,发现结果依然不对,这时就要紧扣同步的俩前提,发现Out中的打印语句也是
* 操作资源,也需要被同步。为了保证锁是同一个锁,就用z对象。至此,打印结果解决了姓名性别不
* 匹配的问题。但是,不能保证输入一个获取一个。因此要考虑使用等待唤醒机制。*/
等待唤醒机制:wait()、notify()、notifyAll();
都使用在同步中,因为要对持有监视器(锁)的线程操作。所以要使用在同步中,因为只有同步才具有锁.用法:锁.方法。 通常,等待线程都在线程池中,notify唤醒的是其中相同锁的第一个被等待的线程。Sleep方法不释放锁,wait方法释放锁。
为什么这些操作线程的方法要定义Object类中呢?
因为这些方法在操作同步中线程时,都必须要标识它们所操作线程只有的锁,只有同一个锁上的被等待线程,可以被同一个锁上notify唤醒。不可以对不同锁中的线程进行唤醒。也就是说,等待和唤醒必须是同一个锁。而锁可以是任意对象,所以可以被任意对象调用的方法定义Object类中。
//使用等待唤醒机制,且对代码优化
public class T11
{
public static void main(String[] args)
{
Z z=new Z();
new Thread(new In1(z)).start();
new Thread(new Out1(z)).start();
}
}
//描述被操作的资源
class Z{
private String name;
private String sex;
private boolean flag=false;
public synchronized void set(String name,String sex)//同步函数,锁是this
{
if(flag)
try{this.wait();}catch(Exception e){}//等待唤醒机制要在同步中使用
this.name=name;
this.sex=sex;
flag=true;
this.notify();
}
public synchronized void get()//同步函数,锁是this
{
if(!flag)
try{this.wait();}catch(Exception e){}
System.out.println(this.name+"-----"+this.sex);
flag=false;
this.notify();
}
}
class In1 implements Runnable
{
private Z z;
In1(Z z)//这只是为了保证能够共用同一个资源
{
this.z=z;
}
public void run()
{
int x=0;
while(true)
{
if(x==0)
z.set("zhangsan","nan");
else
z.set("丽丽丽丽丽丽丽丽","女女女女女");
x=(x+1)%2;
}
}
}
class Out1 implements Runnable
{
private Z z;
Out1(Z z)
{
this.z=z;
}
public void run()
{
while(true)
z.get();
}
}
/*将资源中的属性全部私有,定义访问这些属性的方法。要保证输入一个输出一个的友好顺序,
* 那就需要In执行一次,out执行一次,因此要定义一个判断标记flag,flag为真就输出,
* 为假就输入。因此定义flag作为判断标记。因为为了保证俩线程用的同一个资源,所以
* 只创建了一个资源对象,因此保证了set和get两个同步函数锁一致。又因为等待唤醒机
* 制要在同步中使用,所以定义在这两个同步函数中,锁是this。
* 注意:wait()会释放锁,sleep()不会*/
每个动作都开启多个线程:
//生产者和消费者
public class T12 {
public static void main(String[] args) {
Resource rec=new Resource();
new Thread(new Pro(rec)).start();
new Thread(new Pro(rec)).start();
new Thread(new Cons(rec)).start();
new Thread(new Cons(rec)).start();
}
}
//资源
class Resource
{
private String name;
private int count;
private boolean flag=false;
public synchronized void set(String name)
{
while(flag)//if(flag)
try{this.wait();}catch(Exception e){}
this.name=name+"---"+count++;
System.out.println(Thread.currentThread().getName()+"---生产者---"+this.name);
flag=true;
this.notifyAll();//this.notify();
}
public synchronized void get()
{
while(!flag)//if(!flag)
try{this.wait();}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"-----xiaofeizhe-------"+this.name);
flag=false;
this.notifyAll();//this.notify();
}
}
class Pro implements Runnable
{
private Resource rec;
Pro(Resource rec)
{
this.rec=rec;
}
public void run()
{
while(true)
rec.set("商品");
}
}
class Cons implements Runnable
{
private Resource rec;
Cons(Resource rec)
{
this.rec=rec;
}
public void run()
{
while(true)
rec.get();
}
}
/*解析一下:创建了多个线程来执行同一个动作。假如判断标记还是if语句。此时有0、1两个生产者线程,
* 2、3两个消费者线程.结果会出现消费俩生产一个或者生产俩消费一个的情况
* 第1、假如0有了执行权,flag=false,0先去执行name赋值语句和打印语句,flag=true,此时,
* 0返回if语句,进入等待状态,释放执行权。
* 第2、假如此时1线程得到执行权,进入if语句,也进入等待状态(0等待,1在if下等待)
* 第3、此时2线程得到执行权,正常打印语句,之后flag=false,唤醒0线程,再次在if语句时,进入
* 等待状态。(0被唤醒,1在if下等待,2等待)
* 第4、假如0线程得到了执行权,0进行赋值和输出,此时1被唤醒,0线程继续执行if语句时进入等待状态,
* 此时1线程将继续执行赋值语句和打印语句。问题就出现在这里,问题的根源就是1线程没有再次判断
* 标记flag。
* 所以,为了解决这个问题,将if换成while。
* 但是,由于notify唤醒的是线程池中最先等待的线程,所以有时会唤醒本方线程,从而使所有线程都停在
* 等待状态。所以,将notify换成notifyAll.唤醒所有线程,目的是唤醒对方线程。
* 多个生产者和多个消费者要用while判断标记,用notifyall唤醒线程。*/
生产者和消费者:
对于多个生产者和消费者。
为什么要定义while判断标记。
原因:让被唤醒的线程再一次判断标记。
为什么定义notifyAll。
因为需要唤醒对方线程。因为只用notify,容易出现只唤醒本方线程的情况。导致程序中的所有线程都等待。
JDK1.5 中提供了多线程升级解决方案。将同步Synchronized替换成现实Lock操作。将Object中的wait,notify notifyAll,替换了Condition对象。该对象可以Lock锁 进行获取。该示例中,实现了本方只唤醒对方操作。
在java.util.concurrent.locks包中,有Lock接口,有了这个工具,从原来的一个锁对应一个wait、notify方法转变成一个锁可以对应多个await、signal方法。
Condition是一个接口,由Lock中的newCodition()方法获取对象。通常建立Lock子类对象的方法是 Lock lock=new ReentrantLock();
Lock:替代了Synchronized
lock
unlock //该方法要在finally语句中
newCondition()
Condition:替代了Object wait notify notifyAll
await();
signal();
signalAll();
注意,await方法会拋异常:InterruptedException
import java.util.concurrent.locks.*;
public class T13{
public static void main(String[] args){
Re re=new Re();
//多个生产者和消费者
new Thread(new Sc(re)).start();
new Thread(new Sc(re)).start();
new Thread(new Sc(re)).start();
new Thread(new Sc(re)).start();
new Thread(new Xf(re)).start();
new Thread(new Xf(re)).start();
}
}
class Re{
private String name;
private int count;
private boolean flag;
private Lock lock=new ReentrantLock();
private Condition con1=lock.newCondition();
private Condition con2=lock.newCondition();
public void set(String name)throws InterruptedException
{
lock.lock();//锁
try{
while(flag)//循环判断标记,以免出现不是一生产一消费的情况
con1.await();
this.name=name+"----"+count++;
System.out.println(Thread.currentThread().getName()+"---生产者----"+this.name);
flag=true;
}
finally{
con2.signal();}//唤醒对方
}
public void get()throws InterruptedException
{
lock.lock();
try{
while(!flag)
con2.await();
System.out.println(Thread.currentThread().getName()+"---xiaofei------------"+this.name);
flag=false;
}
finally{
con1.signal();
}
}
}
class Sc implements Runnable{
private Re re;
Sc(Re re){
this.re=re;
}
public void run()
{
while(true)
try{re.set("商品");}catch(InterruptedException i){}
}
}
class Xf implements Runnable{
private Re re;
Xf(Re re){
this.re=re;
}
public void run()
{
while(true)
try{re.get();}catch(InterruptedException i){}
}
}
六、停止线程、interrupt();方法
stop方法已经过时。那么如何停止线程?
只有一种,run方法结束。
开启多线程运行,运行代码通常是循环结构。只要控制住循环,就可以让run方法结束,也就是线程结束。
例如:public class T14 {
public static void main(String[] args) {
Th t=new Th();
Thread t1=new Thread(t);
//t1.setDaemon(true);//把t1设置为守护线程
t1.start(); int i=0;
while(true){
if(i++==30){
t.chenge();//这就控制住了run方法
t1.interrupt();
break;
}
System.out.println("main---线程");}
}
}class Th implements Runnable{
private boolean flag=true;
public void chenge(){
flag=false;
}
/*public void run(){
while(flag)
System.out.println("Th---xiancheng");
}*/
public synchronized void run(){
while(flag)
try{wait();}catch(InterruptedException d){System.out.println("异常");}
System.out.println("Th---xiancheng");
}}/*虽然非同步时能控制run方法,但此时主函数运行完程序就不动了,但程序并未退出,这是由于该线程一直在等待状态,没有被叫醒。所以不能判断标记。所以用interrut方法来终端冻结状态让他强制回到运行状态并且将标记置为假,线程就停下来了,如不置为假,那线程还是挂。。。如果不中断,将t1设置为守护线程也可以将程序停止*/
于是有特殊情况:
当线程处于了冻结状态。就不会读取到标记。那么线程就不会结束。当没有指定的方式让冻结的线程恢复到运行状态是,这时需要对冻结进行清除。强制让线程恢复到运行状态中来。这样就可以操作标记让线程结束。Thread类提供该方法 interrupt();
如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法过程中受阻,则其中断状态将被清除,它还将收到一个 InterruptedException。(也就是说不是通过正常的notify从wait状态中出来,或者时间还没到,睡眠就结束。这也是上述各个方法抛InterruptedException异常的原因)
setDaemon(boolean);
将该线程标记为守护线程或用户线程(后台线程,和前台共同抢劫Cpu资源,其他的跟前台线程一样,但是结束时不一样,前台结束,后台就跟着结束)。当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。
join() throws InterruptedException:等待该线程终止。
join(longmillis) throwsInterruptedException:等待该线程终止的时间最长为 millis 毫秒
join(longmillis, int nanos) throwsInterruptedException:等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
当A线程执行到了B线程的.join()方法时,A就会等待。等B线程都执行完,A才会执行。join可以用来临时加入线程执行。
public class T14 {
public static void main(String[] args)throws InterruptedException {
Thread t1=new Thread(new Th());
Thread t2=new Thread(new Th());
/*t1.start();
t1.join();//此时,主线程释放执行权,等t1执行完main才开始继续执行
t2.start();*/
t1.start();
t2.start();
t1.join();//此时主线程释放执行权,t1和t2抢夺cpu资源,t1执行完主函数再执行
for(int i=0;i<20;i++)
System.out.println("main---线程");
}
}
class Th implements Runnable{
public void run(){
for(int i=0;i<20;i++)
System.out.println(Thread.currentThread().getName()+"--Th---xiancheng");
}}
toString():返回该线程的字符串表示形式,包括线程名称、优先级和线程组。
static voidyield():暂停当前正在执行的线程对象,并执行其他线程。
final voidsetPriority(int newPriority):更改线程的优先级。
优先级影响抢夺cpu的频率。所有自定义的线程的优先级默认是5。
//匿名内部类来创建线程
public class T14 {
public static void main(String[] args)throws InterruptedException {
new Thread(){
public void run(){
for(int i=0;i<50;i++)
System.out.println("main---线程");}}.start();
Runnable r=new Runnable(){
public void run(){
for(int i=0;i<40;i++)
System.out.println("3-------线程");}};
new Thread(r).start();
for(int i=0;i<20;i++)
System.out.println("2-------线程");
}
}