黑马程序员—【Java基础篇】之多线程

时间:2023-02-20 09:58:49
------- android培训java培训、期待与您交流! ---------

    这篇如标题,介绍和总结多线程。

    多线程内容总结,主要分一下几大块:一、进程;二、线程(例:FlashGet);三、多线程存在的意义四、线程的创建方式多线程的特性。

一、进程

1、定义

/**
进程:是一个正在执行中的程序。
每一个进程执行都有一个执行顺序;该顺序是一个执行路径,或者叫一个控制单元。
*/

二、线程

1、定义

/**
线程:就是进程中的一个独立的控制单元。线程在控制着进程的执行。通俗说就是进程是发生的一件事情,而线程就是控制着或者掌控这件事情的发生,一个线程中至少有一个线程。
*/

三、多线程

1、定义

/**
Java VM 启动的时候会有一个进程java.exe.
该进程中至少一个线程负责java程序的执行,而且这个线程运行的代码存在于main方法中,该线程称之为主线程。
扩展:
其实更细节说明jvm,jvm启动不止一个线程,还有负责垃圾回收机制的线程。
*/

2、如何创建多线程

/**
多线程的出现能让程序产生同时运行效果。可以提高程序执行效率。
例如:在java.exe进程执行主线程时,如果程序代码特别多,在堆内存中产生了很多对象,而同时对象调用完后,就成了垃圾。 如果垃圾过多就有可能是堆内存出现内存不足的现象,只是如果只有一个线程工作的话,程序的执行将会很低效;而如果有另一个线程帮助处理的话,如垃圾回收机制线程来帮助回收垃圾的话,程序的运行将变得更有效率。
*/

3、创建方式

(1)继承Thread类

/**
第一步:子类覆盖父类中的run方法,将线程运行的代码存放在run中。
第二步:建立子类对象的同时线程也被创建,通过调用start方法开启线程。
*/

    示例:

<span style="font-size:14px;">class Demo extends Thread//创建第一种方式,继承Thread类。
{
public void run()//覆盖Thread类run()方法。
{
for(int x=0; x<60; x++)
System.out.println("demo run----"+x);
}
}
class ThreadDemo //主函数
{
public static void main(String[] args)
{
Demo d = new Demo();//创建好了一个线程。
d.start();//开启线程并执行该线程的run方法。
//d.run();仅仅是对象调用方法。而线程创建了,并没有运行。
}
}</span>

    练习题:创建2个或以上线程交替运行。

class Test extends Thread//继承了Thread类。
{
Test(String name)
{
super(name);//引用父类构造函数。
}
public void run()//覆盖Thread中run()方法。
{
for(int x=0; x<60; x++)
{
System.out.println((Thread.currentThread()==this)+"..."+this.getName()+" run..."+x);
}
}
}
class ThreadTest
{
public static void main(String[] args)
{
Test t1 = new Test("one---");
Test t2 = new Test("two+++");
t1.start();//线程t1运行;
t2.start();//线程t2运行;
//t1和t2同时运行。
for(int x=0; x<60; x++)//运行一个主线程
{
System.out.println("main....."+x);
}
}
}

(2)实现Runnable接口

/**
1、子类覆盖接口中的run方法。
2、通过Thread类创建线程,并将实现了Runnable接口的子类对象作为参数传递给Thread类的构造函数。
3、Thread类对象调用start方法开启线程。
*/
    示例:
<pre name="code" class="java">class Ticket implements Runnable//实现Runnable接口。
{
private int tick = 100;
public void run()//覆盖方法
{
while(true)
{
if(tick>0)
{
System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
}
}
}
}
class TicketDemo
{
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();
}
}
 
    
 思考:为什么要给Thread类的构造函数传递Runnable的子类对象? 
/**
因为自定义的run方法所属的对象是Runnable接口的子类对象。
所以要让线程去指定指定对象的run方法。就必须明确该run方法所属对象。
*/

(3)两种创建方式区别

 /**
1.当某个类已经有父类了,这时无法用继续Thread方法建立线程,因为JAVA中无法多继承
2.当某个类已经有父类了,这时可以实现Runnable方法建立线程,这样既继承了父类的属性又能建立线程。
3.Runnable建立的多线程可以使用同一对象中封装的数据,而Thread要实现这样的功能必须将数据static化,但会导致数据的生命周期过长。
*/

4、线程的几种状态

    如图:

黑马程序员—【Java基础篇】之多线程
/**
被创建:等待启动,调用start启动。
运行状态:具有执行资格和执行权。
临时状态(阻塞):有执行资格,但是没有执行权。
冻结状态:遇到sleep(time)方法和wait()方法时,失去执行资格和执行权,sleep方法时间到或者调用notify()方法时,获得执行资格,变为临时状态。
消忙状态:stop()方法,或者run方法结束。
注:当已经从创建状态到了运行状态,再次调用start()方法时,就失去意义了,java运行时会提示线程状态异常。
*/
    图解:
黑马程序员—【Java基础篇】之多线程

5、线程的安全问题

(1)多线程安全问题引出

/**
当多个线程操作同一个共享数据时,一个线程对共享数据的多条代码只执行了一部分,还没执行完,而其他线程参与进来,导致共享数据错误。<pre name="code" class="java"> 刚才示例中出现了,卖票程序卖到了-1、-2张票,这是怎么出来的呢?原因如下:
当多条语句在操作同一线程共享数据时,一个线程对多条语句只执行了一部分,还未执行完,另一线程就参与进来执行了,导致共享数据发生错误。也就是说,由于cpu的快速切换,当执行线程一时,tic为1了,执行到if (tic > 0)的时候,cpu就可能将执行权给了线程二,那么线程一就停在这条语句了,tic还没减1,仍为1;线程二也判断if (tic > 0)是符合的,也停在这,以此类推。当cpu再次执行线程一的时候,打印的是1号,执行线程二的时候,是0号票,以此类推,就出现了错票的结果。其实就是多条语句被共享了,如果是一条语句,是不会出现此种情况的。
*/
 

    那么如何解决这个问题,如何呢?ok,不要着急,Java开发者们,考虑到了这种情况,于是提供了一种解决方案,且听我娓娓道来。

(2)解决问题

/**
对于多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可参与执行,就不会出现问题了。Java对于多线程的安全问题,提供了专业的解决方式,即同步代码块,可操作共享数据。
a、同步代码块
用法:
synchronized(对象)
{需要被同步的代码}<pre name="code" class="java">b、同步函数
同步函数就是将修饰符synchronized放在返回类型的前面,下面通过同步函数给出多线程安全问题的具体解决方案:

1)目的:判断程序中是否有安全问题,若有,该如何解决。
2)解决:第一、明确哪些代码是多线程的运行代码
第二、明确共享数据
第三、明确多线程运行代码中,哪些语句是操作共享数据的。
*/

(3)示例

//1.同步代码块示例:
class Ticket implements Runnable
{
private int tick = 1000;
Object obj = new Object();//任意对象
public void run()
{
while(true)
{
synchronized(obj)//同步代码块,obj是一把锁
{
if(tick>0)
{
//try{Thread.sleep(10);}catch(Exception e){}//等待10秒钟
System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
}
}
}
}
}

class TicketDemo2
{
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();
}
}<pre name="code" class="java">//2.同步函数示例:
class Ticket implements Runnable
{
private int tick=100;
Object obj = new Object();
public void run()
{
while(true)
{
show();
}
}
//直接在函数上用synchronized修饰即可实现同步
public synchronized void show()
{
if(tick>0)
{
try
{
//使用线程中的sleep方法,模拟线程出现的安全问题
//因为sleep方法有异常声明,所以这里要对其进行处理
Thread.sleep(10);
}
catch (Exception e)
{
}
//显示线程名及余票数
System.out.println(Thread.currentThread().getName()+"..tick="+tick--);
}
}
}

(4)小结

/**
1. 同步代码块示例中:
其中的对象如同锁,持有锁的线程可在同步中执行,没有锁的线程,即使获得cpu的执行权,也进不去,因为没有获取锁,是进不去代码块中执行共享数据语句的。
1)同步的前提:

a.必须有两个或两个以上的线程

b.必须保证同步的线程使用同一个锁,必须保证同步中只能有一个线程在运行。

好处与弊端:解决了多线程的安全问题,多个线程需要判断锁,较为消耗资源。
2. 同步函数
格式:
在函数上加上synchronized修饰符即可。
那么同步函数用的是哪一个锁呢?
函数需要被对象调用。那么函数都有一个所属对象引用。就是this。所以同步函数使用的锁是this。
*/

(5)静态函数的同步方式

/**
如果同步函数被静态修饰后,经验证,使用的锁不是this了,因为静态方法中不可定义this,所以,这个锁不再是this了。静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象:类名.class;该对象的类型是Class。
所以静态的同步方法使用的锁是该方法所在类的字节码文件对象,即类名.class。
*/
    示例:
/* 
加同步的单例设计模式————懒汉式
*/
class Single
{
private static Single s = null;
private Single(){}
public static void getInstance()
{
if(s==null)
{
synchronized(Single.class)
{
if(s==null)
s = new Single();
}
}
return s;
}
}

6、死锁

    当同步中嵌套同步时,就有可能出现死锁现象。

    示例:

/*
死锁。
同步中嵌套同步。

*/
class Ticket implements Runnable
{
private int tick = 1000;
Object obj = new Object();
boolean flag = true;
public void run()
{
if(flag)
{
while(true)
{
synchronized(obj)
{
show();
}
}
}
else
while(true)
show();
}
public synchronized void show()//this
{
synchronized(obj)
{
if(tick>0)
{
try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"....code : "+ tick--);
}
}
}
}
class DeadLockDemo
{
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(Exception e){}
t.flag = false;
t2.start();
}
}
     原因分析
/**
分析死锁的原因:当t1、t2两个线程同时运行时,t1执行到代码到lockb时,t2抢夺了CPU的执行权,t1执行过程暂停,而t2在执行到locka时又被t1抢夺了执行权,但当t1执行到lockb时因为锁在t2手里没有释放,t1无法继续执行,同理t2,这样造成了程序死锁。
*/

7、线程间通信

(1)线程间通信初步解决

<span style="font-size:14px;">/**</span>
多线程间的通信:将不同功能的代码封装在不同的线程代码中操作共同的资源。
实现的思想:
1.两线程实现Runnable接口,必须传入同一个对象
2.建立一资源类,设置成员变量,就是线程中的共享数据
3.在实现Runnable的子类中,将资源类名设置为成员变量,设置构造函数传入的参数为资源类型的变量,复写run()方法,操作资源数据。
*/
    示例1:
<span style="font-size:12px;">class Resource
{
private String name;
private int count = 1;
private boolean flag = false;
// t1 t2
public synchronized void set(String name)
{
while(flag)
try{this.wait();}catch(Exception e){}//t1(放弃资格) t2(获取资格)
this.name = name+"--"+count++;

System.out.println(Thread.currentThread().getName()+"...生产者.."+this.name);
flag = true;
this.notifyAll();
}
// t3 t4
public synchronized void out()
{
while(!flag)
try{wait();}catch(Exception e){}//t3(放弃资格) t4(放弃资格)
System.out.println(Thread.currentThread().getName()+"...消费者........."+this.name);
flag = false;
this.notifyAll();
}
}

class Producer implements Runnable
{
private Resource res;

Producer(Resource res)
{
this.res = res;
}
public void run()
{
while(true)
{
res.set("+商品+");
}
}
}

class Consumer implements Runnable
{
private Resource res;

Consumer(Resource res)
{
this.res = res;
}
public void run()
{
while(true)
{
res.out();
}
}
}
class ResourceDemo2
{
public static void main(String[] args)
{
Resource r = new Resource();//表示操作的是同一个资源

new Thread(new Input(r)).start();//开启存线程

new Thread(new Output(r)).start();//开启取线程
}
}
/**
解决方式: Wari()——notifyall(),判断条件为while
判断条件为while后,每次都要判断后才能wait,而notifyall可以将等待的输出线程全部唤醒。
分析示例发现了几个小问题:
1)wait(),notify(),notifyAll(),用来操作线程为什么定义在了Object类中?
a,这些方法存在与同步中。
b,使用这些方法时必须要标识所属的同步的锁。同一个锁上wait的线程,只可以被同一个锁上的notify唤醒。
c,锁可以是任意对象,所以任意对象调用的方法一定定义Object类中。
2)wait(),sleep()有什么区别?
wait():释放cpu执行权,释放锁。
sleep():释放cpu执行权,不释放锁。
3)为甚么要定义notifyAll?
因为在需要唤醒对方线程时。如果只用notify,容易出现只唤醒本方线程的情况。导致程序中的所以线程都等待。
*/</span>

(2)JDK1.5中提供了多线程升级解决方案

    将同步synchronized替换成显示的Lock操作。将Object中wait,notify,notifyAll,替换成了Condition对象。该Condition对象可以通过Lock锁进行获取,并支持多个相关的Condition对象。

    示例2:

/* 
线程间通信: 等待唤醒机制:升级版 ;生产者消费者 多个

*/
import java.util.concurrent.locks.*;

class ProducerConsumerDemo{
public static void main(String[] args){
Resouse r = new Resouse();
Producer p = new Producer(r);
Consumer c = new Consumer(r);
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
Thread t3 = new Thread(p);
Thread t4 = new Thread(c);
t1.start();
t2.start();
t3.start();
t4.start();
}
}

class Resouse{
private String name;
private int count = 1;
private boolean flag = false;
private Lock lock = new ReentrantLock();
private Condition condition_P = lock.newCondition();
private Condition condition_C = lock.newCondition();
//要唤醒全部,否则都可能处于冻结状态,那么程序就会停止。这和死锁有区别的。
public void set(String name)throws InterruptedException{
lock.lock();
try{
while(flag)//循环判断,防止都冻结状态
condition_P.await();
this.name = name + "--" + count++;
System.out.println(Thread.currentThread().getName() + "..生成者--" + this.name);
flag = true;
condition_C.signal();
}finally{
lock.unlock();//释放锁的机制一定要执行
}
}
public void out()throws InterruptedException{
lock.lock();
try{
while(!flag)//循环判断,防止都冻结状态
condition_C.await();
System.out.println(Thread.currentThread().getName() + "..消费者." + this.name);
flag = false;
condition_P.signal();//唤醒全部
}finally{
lock.unlock();
}
}
}

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

class Consumer implements Runnable{
private Resouse r;
Consumer(Resouse r){
this.r = r;
}
public void run(){
while(true){
try{
r.out();
}catch (InterruptedException e){}
}
}
} <pre name="code" class="java">/**
示例说明:
1)、为何定义while判断标记:
原因是让被唤醒的线程再判断一次。
避免未经判断,线程不知是否应该执行,就执行本方的上一个已经执行的语句。如果用if,消费者在等着,两个生成着一起判断完flag后,cpu切换到其中一个如t1,另一个t3在wait,当t1唤醒冻结中的一个,是t3(因为它先被冻结的,就会先被唤醒),所以t3未经判断,又生产了一个。而没消费。

2)这里使用的是signal方法,而不是signalAll方法。是因为通过Condition的两个对象,分别唤醒对方,这就体现了Lock锁机制的灵活性。可以通过Contidition对象调用Lock接口中的方法,就可以保证多线程间通信的流畅性了。
*/

8、Thread中的其它方法

/**
1、停止线程:
在java 1.5之后,就不再使用stop方法停止线程了。那么该如何停止线程呢?只有一种方法,就是让run方法结束。
开启多线程运行,运行代码通常为循环结构,只要控制住循环,就可以让run方法结束,也就可以使线程结束。
注:
特殊情况:当线程处于冻结状态,就不会读取标记,那么线程就不会结束。如下:
*/
class StopThread implements Runnable{
private boolean flag = true;
public synchronized void run(){
while (flag){
try{
wait();
}catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "----Exception");
flag = false;
}
System.out.println(Thread.currentThread().getName() + "----run");
}
}
public void changeFlag(){
flag = false;
}
}

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

Thread t1 = new Thread(st);
Thread t2 = new Thread(st);

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

int n = 0;
while (true){
if (n++ == 60){
st.changeFlag();
break;
}
System.out.println("Hello World!");
}
}
}
/**
这时,当没有指定的方式让冻结的线程回复打破运行状态时,就需要对冻结进行清除。强制让线程回复到运行状态来,这样就可以操作标记让线程结束。
在Thread类中提供了此种方法:interrupt()。此方法是为了让线程中断,但是并没有结束运行,让线程恢复到运行状态,再判断标记从而停止循环,run方法结束,线程结束。
*/
class StopThread implements Runnable{
private boolean flag = true;
public synchronized void run(){
while (flag){
try{
wait();
}catch (InterruptedException e){
System.out.println(Thread.currentThread().getName() + "----Exception");
flag = false;
}
System.out.println(Thread.currentThread().getName() + "----run");
}
}
}

class StopThreadDemo{
public static void main(String[] args){
StopThread st = new StopThread();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
t1.start();
t2.start();
int n = 0;
while (true){
if (n++ == 60){
t1.interrupt();
t2.interrupt();
break;
}
System.out.println("Hello World!");
}
}
}
/**
2、守护线程:---setDaemon()
可将一个线程标记为守护线程,直接调用这个方法。此方法需要在启动前调用守护线程在这个线程结束后,会自动结束,则Jvm虚拟机也结束运行。
守护线程(后台线程),在启动前调用。后台线程自动结束。
*/
t1.setDaemon(true);
t2.setDaemon(true);
t1.start();
t2.start();

/**
3、临时加入线程:--join()
特点:当A线程执行到B线程方法时,A线程就会等待,B线程都执行完,A才会执行。join可用来临时加入线程执行。
*/
class Demo implements Runnable{
public void run(){
for(int x=0;x<90;x++){
System.out.println(Thread.currentThread().getName() + "----run" + x);
}
}
}

class JoinDemo{
public static void main(String[] args)throws Exception{
Demo d = new Demo();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();
t2.start();
t1.join();//等t1执行完了,主线程才从冻结状态恢复,和t2抢执行权。t2执不执行完都无所谓。
int n = 0;
for(int x=0;x<80;x++){
System.out.println(Thread.currentThread().getName() + "----main" + x);
}
System.out.println("Over");
}
}
/**
4、优先级:setPriority():
在Thread中,存在着1~10这十个执行级别,最高的是 MAX_PRIORITY 为10,最低是 MIN_PRIORITY 为1,默认优先级是 NORM_PRIORITY 为5;但是并不是优先级越高,就会一直执行这个线程,只是说会优先执行到这个线程,此后还是有其他线程会和此线程抢夺cpu执行权的。
优先级是可以设定的,可通过setPriority()设定,如:setPriority(Thread.MAX_PRIORITY)设优先级为最大。
yield():
此方法可暂停当前线程,而执行其他线程。通过这个方法,可稍微减少线程执行频率,达到线程都有机会平均被执行的效果。如下:
*/
class Demo implements Runnable{
public void run(){
for(int x=0;x<90;x++){
System.out.println(Thread.currentThread().toString() + "----run" + x);
Thread.yield();//稍微减少线程执行频率。可达到线程都有机会达到平均运行的效果
}
}
}

class YieldDemo{
public static void main(String[] args)throws Exception{
Demo d = new Demo();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();
t1.setPriority(Thread.MAX_PRIORITY);//设置线程优先级最大
t2.start();
System.out.println("Over");
}
}
    这篇就到这里吧,下篇咱们再见。


------- android培训java培训、期待与您交流! ---------