Java基础:多线程

来源:互联网 发布:js获取被选中的radio 编辑:程序博客网 时间:2024/05/17 08:32

第一部分:进程与线程

一、定义

        进程,是一个正在执行中的程序。每一个进程都有一个执行顺序,该顺序是一个执行路径或者控制单元。一个进程中至少会有一条执行路径,即控制单元,叫做线程。

        多线程的存在让程序(代码)产生了同时运行的效果,提高了运行的效率。迅雷就是一个多线程程序。

二、多线程

        电脑中的进程可以同时开启,由CPU执行,CPU在这些进程之间进行快速的切换,而真正切换的,就是线程。

        Java中也有对多线程的实现方式,可以同时开启多个线程。(进程由操作系统创建,进程中的线程也由系统创建,因此JVM只要调用操作系统的这些功能就可以了)

        Java在运行时会有一个进程java.exe,负责java程序的执行,而且这个线程运行的代码存于main方法中,该线程称之为主线程。

        更细节的说JVM不只一个线程(主线程,代码存放于main函数中,这就是JVM为何要调用main方法的原因),还有负责垃圾回收的线程。

三、线程的状态

第二部分:线程的创建方式

一、继承thread类

Thread:程序中的执行线程,JVM运行应用程序并发的执行多个执行线程。

步骤:

1、定义类继承Thread。

2、复写Thread类中的run方法。(用于存储线程要执行的代码)

3、调用线程的start方法开启线程,并执行run方法中的代码。

package com.itheima;/*创建一个新执行线程方法:1、定义类继承Thread类2、复写Thread类中的run方法   目的:将自定义的代码存储在run方法中,让线程运行。3、调用线程中的start方法   该方法两个作用,启动线程  调用run方法*/class Demo extends Thread{public void run(){for(int x=0;x<60;x++){System.out.println("Demo Run :"+x);}}}public class  ThreadDemo{public static void main(String[] args) {Demo d = new Demo();d.start();//开启线程并执行该线程的run方法//d.run();只有调用run方法,由主线程执行(d线程创建了并没有运行)for(int x=0;x<60;x++)//主线程{System.out.println("Main Run:"+x);}}}

结果分析:

        代码在运行时,多个线程都在获取CPU的执行权,CPU执行到哪个线程,哪个线程就运行,准确的说,在某一时刻,只有一个程序在运行(多核除外),CPU在做着快速的切换,以达到看上去是同时运行的效果。

二、实现Runnable接口

Runnable:接口,用于确立线程所运行代码存放的位置,其只有一个方法:run()方法。Thread类也是实现了该接口。

步骤:

1、定义类实现Runnable接口。

2、覆盖Runnable接口中的run方法,并将要运行的代码存放到run方法中。

3、通过Thread类建立对象。

4、将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。

5、调用Thread类的start方法开启线程。(自动调用Runnable接口中的run方法)

package com.itheima;class Test implements Runnable //定义类实现Runnable接口{int num = 60;public void run()//覆盖run方法{while(num>0){System.out.println(Thread.currentThread().getName()+"----"+num--);}}}public class TicketDemo{public static void main(String[] args){Test t = new Test();//新建2个线程,并将Runnable接口子类对象作为参数传递给Thread类的构造函数Thread t1 = new Thread(t);Thread t2 = new Thread(t);//开启这2个线程t1.start();t2.start();}}

实现的好处:避免了继承的局限性,因此开发中一般使用实现的方式。

第三部分:多线程的安全问题

1、出现安全问题的原因

        当多条语句在操作同一线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另外一个线程参与进来执行,导致共享数据错误,出现完全问题。

/* * 同步的应用     * 需求:两个用户往银行存钱,每次存100,每人存300块钱。 * 思路;   *     由于操作共享数据的 13与22行数据是分开的,多线程在执行时就出现了安全问题,故采用同步代码块或者同步函数 *///描述银行class Bank{int sum;public /*synchronized*/ void add(int num){sum +=num;//父类的run方法未抛出异常,只能覆盖只能try 不能抛try{Thread.sleep(10);//当前线程睡10毫秒}catch(InterruptedException i){i.printStackTrace();}System.out.println("sum = "+sum);}}//描述储户class Cus implements Runnable{Bank b = new Bank();public void run(){for(int x=0;x<3;x++){b.add(100);//存100块钱}}}public class BankDemo {public static void main(String[] args) {//定义一个储户Cus cus = new Cus();//开启线程new Thread(cus).start();new Thread(cus).start();}}

加入同步前:

取消注释,加入同步后:

2、同步代码块

1)格式

synchronized(对象)

{

      需要被同步的代码;

}

        synchronized相当于一把锁,当同步代码块中有线程在执行时,其他线程无法进入,必须要等到同步代码块中的线程执行完毕,释放锁后,其他线程才可以进入。

        对象如同锁,只有锁的线程可以在同步代码块中执行,没有锁的线程即使获取到了CPU的执行权,也无法进入同步代码块中,因为没有锁。

2)同步的前提

(a)必须要有两个以上的线程

(b)必须是多个线程使用同一个锁

注意:不要把run方法中的所有代码都存放在同步里面,这样就相当于单线程程序了。

3)同步的好处与弊端

好处:解决了多线程的安全问题。

弊端:消耗了资源,因为每次都要判断锁,即同步代码块中是否有线程正在执行。

4)同步函数

当synchronized被放在函数上时,该函数就具备了同步性,如:public synchronized void show()

(a)非静态同步函数的锁是this。(因为函数需要被对象调用,而函数存在的所属对象的引用就是this)

(b)静态同步函数的锁是该类对应的字节码文件对象。(因为静态加载时,对象不存在,但一定有该类对应的class文件)

单例设计模式中懒汉式的代码:/* * 单例设计模式:懒汉式对象的延时加载 *  * 同步函数的锁是 this * 静态同步函数的锁是 该类对应的字节码文件 .class * 由于静态函数中不可能出现this关键字(静态优先于类加载),因此静态同步函数的锁也是:类名.class */public class Single {private static Single s = null;private Single(){}public static Single getInstance(){if(s==null){synchronized(Single.class){/* * 多条语句操作共享数据,给这多条语句加上同一个锁,但是加上锁后效率变低 * 故再在外面加一层判断语句,以后就不会在判断锁了,提高了懒汉式的效率 */if(s==null)s = new Single();}}return s;}}

懒汉式的特点:

(a)对象的延时加载。

(b)延时加载在多线程访问时出现安全隐患

(c)通过加入同步来解决安全问题,由于加入同步后效率较低,故加入双重判断语句以提高效率。

(d)加的锁是该类所对应的字节码文件对象。

3、死锁

同步嵌套使用时,会出现死锁的情况。

/* * 多线程的死锁演示 * 同步代码块嵌套时出现死锁。 */class MyLock{//定义两个锁static Object lockA = new Object();static Object lockB = new Object();}class Text implements Runnable{//定义指针private boolean flag;//构造时指定指针的值Text(boolean flag){this.flag = flag;}//线程运行代码public void run(){if(flag){while(true){//t1拿着A锁,想要B锁synchronized(MyLock.lockA){System.out.println("if --- A");synchronized(MyLock.lockB){System.out.println("if --- B");}}}}else{while(true){//t2拿着B锁想要A锁synchronized(MyLock.lockB){System.out.println("else --- B");synchronized(MyLock.lockA){System.out.println("else --- A");}}}}}}public class DeadLock{public static void main(String[] args){//定义两个Text对象,并初始化指针Text text1 = new Text(true);Text text2 = new Text(false);//开启线程,两个线程运行的内容不同,t1运行if中的内容,t2运行else中的内容。Thread t1 = new Thread(text1);Thread t2 = new Thread(text2);t1.start();t2.start();}} 

运行结果:


第四部分:多线程间的通信

当多个线程操作同一个资源,但是操作的动作不同。如图


示例代码:

package demo;/* * 需求:对资源进行操作,输入一次就打印异常 * 思路:采用多线程对同一个资源进行操作,但是操作的动作不同,即:一个线程负责存入,一个线程负责取出。 * 故要加入同步代码块。 *///资源class Resource{String name;String sex;}class Input implements Runnable{int num;Resource r;//构造时接收资源对象Input(Resource r){this.r = r;}public void run(){while(true){synchronized(r){//对资源的name与sex属性进行设置if(num==0){r.name = "燕子";r.sex = "女";}else{r.name = "Jack Sprrow";r.sex = "man";}num = (num+1)%2;}}}}class Output implements Runnable{Resource r;//构造时接收资源对象Output(Resource r){this.r = r;}public void run(){while(true){synchronized(r){//对资源进行打印System.out.println(r.name+"....."+r.sex);}}}}public class InputOutputDemo {public static void main(String[] args){Resource r = new Resource();//将同一资源作为参数分别传递给Input与Output的构造函数Input in = new Input(r);Output out = new Output(r);//开启线程new Thread(in).start();new Thread(out).start();}}
运行结果:



1、等待唤醒机制

1)单个输入与输出

        由上述结果可以看出,输入与输出并不是一一对应的关系,即输入线程在获取到CPU的执行权后疯狂的存入,输出线程在获取到CPU的执行权后疯狂的输出。为了解决这种情况的出现,给资源加入一个指针flag,输入线程与输出线程在执行时都要先判断指针,并加入等待唤醒机制,如图:

        所谓等待唤醒机制,就是在操作共享数据时,为了让输入与输出一一对应的解决方案。所涉及方法有:(均来自Object类中,会抛出InterruptedException)

void wait()        持有指定锁的线程等待,并释放执行权

void notify()        唤醒线程池中第一个被等待的线程

void notifyAll()        唤醒线程池中的所有线程

注意:

(a)wait(),notify(),notifyAll()均使用在同步中,因通常都是对持有监视器(锁)的线程操作,所以要使用在同步中,因为只有同步中有锁的概念。

(b)为何操作线程的方法都要定义在Object类中呢?

        因为这些方法在操作同步中的线程时,都必须要标识他们所操作线程所属的锁,只有同一个锁上的被等待线程可以被同一锁上的notify()或notifyAll()唤醒,不可以对不同锁中的线程进行唤醒,不可以对不同锁中的线程进行唤醒。也就是说,等待与唤醒必须是同一把锁。而锁可以是任意对象,所以可以被任意对象操作的方法定义在Object类中。

package day.day2;class Res{private String name;private String sex;private boolean flag = false;//设置姓名(输入)public synchronized void set(String name,String sex){//判断指针,为真(有数据)就等待,假(无数据)就存数据if(flag)try{this.wait();}catch(Exception e){e.printStackTrace();}this.name = name;this.sex = sex;//存入数据后,更改指针,并唤醒其他线程flag = true;this.notify();}//打印线程(输出)public synchronized void get(){//判断指针,无数据(假)就等待,有数据(真)就取出if(!flag)try{this.wait();}catch(Exception e){e.printStackTrace();}System.out.println(name+"....."+sex);//取出数据后,更改指针,并唤醒其他线程flag = false;this.notify();}}class Input implements Runnable{Res s;Input(Res s){this.s = s;}public void run(){int num = 0;while(true){synchronized(s){if(num==0){s.set("Jack Sprrow","man");}else{s.set("燕子","女");}num = (num+1)%2;}}}}class Output implements Runnable{private Res s;Output(Res s){this.s = s;}public void run(){while(true){synchronized(s){s.get();}}}}public class InputOutputDemo{public static void main(String[] args){Res r = new Res();//资源new Thread(new Input(r)).start();//输入线程new Thread(new Output(r)).start();//输出线程}}

运行结果:

2)多个输入与输出

先看一个生产者与消费者的例子:

package demo;/* * 多个不同线程操作同一个资源 */class Resource{private String name;private int count = 1;private boolean flag;public synchronized void set(String name){if(flag)try{wait();//t1放弃资格t2获取资格/* * t1/t3/t4都放弃资格,t1 wait前唤醒的只有t2 若为if则不再判断标记 向下执行,因为是在当前位置等待的 * 因此要循环判断标记,循环判断标记后,容易出现所有线程都等待的情况 *  * if只能用于生产者一个消费者一个的情况(即可以保证唤醒的是对方线程) */}catch(InterruptedException e){e.printStackTrace();}this.name = name+".."+count++;System.out.println(Thread.currentThread().getName()+"....生产者...."+this.name);flag = true;notify();/* * 采用notify 容易只唤醒本边的等待线程,因此notifyAll */}public synchronized void out(){if(!flag)try{wait();//t3放弃资格t4放弃资格}catch(InterruptedException e){e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"....消费者............"+this.name);flag = false;notify();}}class Producer implements Runnable{Resource r;Producer(Resource r){this.r = r;}public void run(){while(true){r.set("商品");}}}class Consumer implements Runnable{Resource r;Consumer(Resource r){this.r = r;}public void run(){while(true){r.out();}}}public class ProduceConsume {public static void main(String[] args){Resource r = new Resource();Producer p = new Producer(r);//生产者Consumer c = new Consumer(r);//消费者//两个生产者线程,两个消费者线程Thread t1 = new Thread(p);Thread t2 = new Thread(p);Thread t3 = new Thread(c);Thread t4 = new Thread(c);t1.start();t2.start();t3.start();t4.start();}}
运行结果:


通过分析结果发现:

(a)两次输入对应一次输出,说明没有判断指针。(其实是notify时,只唤醒了本方线程,由于已经判断指针,不再判断,向下执行),故采用while循环来循环判断指针。

(b)循环判断指针后当notify的线程还是本方线程时,所有线程都会等待,不会继续输入与输出,类似死锁,因此采用notifyAll唤醒线程池中的所有等待线程。

代码修改后,输入与输出一一对应。

package demo;/* * 多个不同线程操作同一个资源 */class Resource{private String name;private int count = 1;private boolean flag;public synchronized void set(String name){while(flag)//循环判断标记try{wait();}catch(InterruptedException e){e.printStackTrace();}this.name = name+".."+count++;System.out.println(Thread.currentThread().getName()+"....生产者...."+this.name);flag = true;notifyAll();//唤醒线程池中的所有等待线程}public synchronized void out(){while(!flag)//循环判断标记try{wait();}catch(InterruptedException e){e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"....消费者............"+this.name);flag = false;notifyAll();//唤醒线程池中的所有等待线程}}class Producer implements Runnable{Resource r;Producer(Resource r){this.r = r;}public void run(){while(true){r.set("商品");}}}class Consumer implements Runnable{Resource r;Consumer(Resource r){this.r = r;}public void run(){while(true){r.out();}}}public class ProduceConsume {public static void main(String[] args){Resource r = new Resource();Producer p = new Producer(r);Consumer c = new Consumer(r);//开启两个生产者线程,两个消费者线程new Thread(p).start();new Thread(p).start();new Thread(c).start();new Thread(c).start();}}
运行结果:



2、显示锁机制(JDK1.5的新特性)

        java.util.concurrent.locks包中有一个Lock接口,该接口实现提供了比使用synchronized方法和语句可获得更广泛的锁定操作。

此实现允许更灵活的结构,可以具有差别很大的属性,一个锁可以支持多个Condition对象。

该接口中有以下方法:

void lock()         获取锁

void unlock()        释放锁(该方法要定义在finally中)

Condition newCondition()        返回绑定到到此Lock实例的Condition实例

synchronized对应lock()与unlock()方法;

Condition类有以下方法:(对应Object类中的wait()、notify()、notifyAll()方法,同样会抛出InterruptedException)

void await()

void signal()

void signalAll()

使用Lock接口中的方法时,可以建立子类(ReentrankLock)对象使用。

/*Jdk 1.5新特性一个锁对应多个condition 对象,可以实现唤醒指定线程wait与await 均抛出异常 InterruptedExceptionfinally 中 有lock.unlock的动作*/package day.day1;import java.util.concurrent.locks.*;class Resource{private String name;private int count;private boolean flag;//定义一个锁ReentrantLock lock = new ReentrantLock();//一个锁对应两个Condition对象Condition condition_pro = lock.newCondition();Condition condition_con = lock.newCondition();public void set(String name){try{//获取锁lock.lock();while(flag){//生产者线程等待condition_pro.await();}this.name = name+"..."+count++;System.out.println(Thread.currentThread().getName()+"..生产者.."+this.name);flag = true;//唤醒消费者线程condition_con.signal();}catch (InterruptedException e){e.printStackTrace();}finally{//一定要执行释放锁的动作lock.unlock();}}public void out(){try{//获取锁lock.lock();while(!flag)condition_con.await();System.out.println(Thread.currentThread().getName()+"..消费者........."+this.name);flag = false;//唤醒生产者线程condition_pro.signal();}catch (InterruptedException e){e.printStackTrace();}finally{//锁的动作一定执行lock.unlock();}}}class Producer implements Runnable{Resource r;Producer(Resource r){this.r = r;}public void run(){while(true){r.set("商品");}}}class Consumer implements Runnable{Resource r;Consumer(Resource r){this.r = r;}public void run(){while(true){r.out();}}}public class ProducerConsumer{public static void main(String[] args){Resource r = new Resource();Producer pro = new Producer(r);Consumer con = new Consumer(r);Thread t1 = new Thread(pro);Thread t2 = new Thread(pro);Thread t3 = new Thread(con);Thread t4 = new Thread(con);t1.start();t2.start();t3.start();t4.start();}}

第五部分:停止线程

停止线程有两种方式:1、调用Thread类中的stop()方法;2、run()方法结束。但是stop()方法已过时。

由于线程中一般都会写循环,结束循环后就可以接收run方法。

1、定义循环结束标记。

2、使用Thread类的interrupt方法,将强制清除冻结状态,但是会抛出InterruptedException。

package day.day3;/* * 停止线程 * 1、定义循环结束标记(线程一般都会用到循环) * 该方法在所有线程都wait(存在于同步中)时,线程冻结,无法判断标记,那么线程就不会结束 * 2、使用Thread类提供的Interrupt方法,可以让所指定的处于冻结状态的线程回到运行状态中来 * 但是Interrupt方法是强制结束线程的冻结状态,故在使用它时,会抛出InterruptedException异常 * 如果是wait处的线程冻结就在wait处抛出该异常 */class StopThread implements Runnable{private boolean flag = true;public void changeFlag(){this.flag = false;}public synchronized void run(){while(flag){try{//线程运行到次就等待,之后及时更改指针也无用,因为线程还处于冻结状态wait();}catch(InterruptedException e){System.out.println(Thread.currentThread().getName()+"......Exception");flag = false;//捕捉到异常后,更改改标记,结束循环,随后run方法结束}System.out.println(Thread.currentThread().getName()+"...run");}}}public 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 x = 0;while(true){if(x++==60){//强制终端指定线程的冻结状态,会抛出InterruptedExceptiont1.interrupt();t2.interrupt();break;}System.out.println(Thread.currentThread().getName()+"..."+x);}}}

第六部分:常见方法

Thread类中:

void setDaemon(Boolean on)    

          on==true时,将该线程设置为守护线程,主线程结束后,所有守护线程都结束;当正在运行的线程都是守护线程时,JVM退出

void join()        加入线程(当A线程执行到B线程的join方法时,A线程就会等待,直到B线程结束后,A线程才会继续执行)

String toString()        返回该线程的线程名称、优先级、线程组的字符串

void setPriority(int newPriority)        设置优先级,共10级。MAX_PRIORITY,MIN_PRIORITY,NORM_PRIORITY

static void yield()        暂停当前正在执行的线程,并执行其他线程

static Thread currentThread()        获取当前线程

String getName()        获取线程名称

void setName(String name)        设置线程名称

构造方法:

Thread(String name)        构造时设置线程名称

Thread(Runnable target,String name)        构造时传入Runnable子类对象和线程名称

开发中何时使用多线程:

        当一个程序有多个循环,每个循环的数据量都很大时,若采用单线程,后面的代码几乎执行不到,故当某些代码需要同时被执行时,就用单独的线程封装,同时可以采用匿名内部类的方式进行封装。

public class ThreadDemo{public static void main(String[] args){for(int x=0;x<4000;x++){System.out.println("x = "+x);}//采用匿名内部类的方式新开一个线程new Thread(){public void run(){for(int y=0;y<3000;y++)System.out.println("y = "+y);}}.start();}}
0 0
原创粉丝点击