Java之多线程、线程池

来源:互联网 发布:淘宝助理苹果电脑版 编辑:程序博客网 时间:2024/05/17 20:30

多线程

线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少一个线程(main),但是可以有多个线程的,则这个程序称为多线程程序。每一个线程执行不同的任务,多线程程序并不能提高程序的运行速度,可以提高程序的运行效率。

CPU处理可以分为分时调度和抢占式调度,分时调度:即线程是轮流被CPU处理器所处理,处理时间大致相同,抢占式调度:即让优先级最高的线程使用CPU,如果优先级相同,则会随机选择一个线程。Java就是使用抢占式调度。

主线程:

Java虚拟机是从程序中的main方法开始执行,按照程序代码的顺序来执行,一直到程序结束,这个线程就是主线程。

但是在实际开发中单线程程序并不能实际解决问题,往往需要有多个线程,分别执行不同的任务,并且同时进行,且互不干扰,则就需要借助多线程技术。

多线程:

在API中可以查到创建线程的类被称为Thread类,此类继承Object并实现Runnable类。

多线程执行时,在内存中是如何运行的呢?

多线程执行时,Java虚拟机先找到main方法,使其进入栈中,若程序中含有创建线程的代码,并启动线程,如x.start() ,则这个新线程进入另一个新的栈中,当执行线程的任务结束后,线程在栈内存中自动释放,当所有的线程结束后,则进程结束。

Thread类获取线程名称方法:

String getName() 返回该线程的名称。

static Thread currentThread()  返回对当前正在执行的线程对象的引用。

实例:

class MyThread extends Thread {  //继承ThreadMyThread(String name){super(name);}//复写其中的run方法public void run(){for (int i=1;i<=20 ;i++ ){System.out.println(Thread.currentThread().getName()+",i="+i);}}}class ThreadDemo {public static void main(String[] args) {//创建两个线程任务MyThread d = new MyThread();MyThread d2 = new MyThread();d.run();//没有开启新线程, 在主线程调用run方法d2.start();//开启一个新线程,新线程调用run方法//d.getName();}}

创建线程有两个方法:

1. 通过创建一个继承Thread类的子类,重写Thread类中的run() 方法,并新建该子类的一个对象,然后再调用start()方法即可。

2. 通过声明实现Runnable接口的类,并实现run() 方法,然后新建该子类的一个对象,调用start() 方法。

创建线程方式一:

1 定义一个类继承Thread。

2 重写run方法。

3 创建子类对象,就是创建线程对象。

4 调用start方法,开启线程并让线程执行,同时还会告诉jvm去调用run方法。

实例:

public class Demo01 {public static void main(String[] args) {//创建自定义线程对象MyThread mt = new MyThread("新的线程!");//开启新线程mt.start();//在主方法中执行for循环for (int i = 0; i < 10; i++) {System.out.println("main线程!"+i);}}}
Thread继承类代码:

public class MyThread extends Thread {//定义指定线程名称的构造方法public MyThread(String name) {//调用父类的String参数的构造方法,指定线程的名称super(name);}/** * 重写run方法,完成该线程执行的逻辑 */@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(getName()+":正在执行!"+i);}}}

代码中的start()方法:使该线程开始执行;Java 虚拟机调用该线程的 run 方法。

创建线程方式二:

创建线程的步骤。

1、定义类实现Runnable接口。

2、覆盖接口中的run方法。。

3、创建Thread类的对象

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

5、调用Thread类的start方法开启线程。

实例:

public class Demo02 {public static void main(String[] args) {//创建线程执行目标类对象MyRunnable runn = new MyRunnable();//将Runnable接口的子类对象作为参数传递给Thread类的构造函数Thread thread = new Thread(runn);Thread thread2 = new Thread(runn);//开启线程thread.start();thread2.start();for (int i = 0; i < 10; i++) {System.out.println("main线程:正在执行!"+i);}}}
接口实现类:
public class MyRunnable implements Runnable{//定义线程要执行的run方法逻辑@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println("我的线程:正在执行!"+i);}}}

两种方法进行比较:

第二种方式实现Runnable接口避免了单继承的局限性,所以较为常用。实现Runnable接口的方式,更加的符合面向对象,线程分为两部分,一部分线程对象,一部分线程任务。继承Thread类,线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,既是线程对象,有又有线程任务。实现runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。Runnable接口对线程对象和线程任务进行解耦。


线程安全:

如果多个线程在同时运行,这些线程可能会同时运行同一个数据,则可能会出现错误。

电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “功夫熊猫3”,本次电影的座位共100个(本场电影只能卖100张票)。

我们来模拟电影院的售票窗口,实现多个窗口同时卖 “功夫熊猫3”这场电影票(多个窗口一起卖这100张票)。

实例:

public class ThreadDemo {public static void main(String[] args) {//创建票对象Ticket ticket = new Ticket();//创建3个窗口Thread t1  = new Thread(ticket, "窗口1");Thread t2  = new Thread(ticket, "窗口2");Thread t3  = new Thread(ticket, "窗口3");t1.start();t2.start();t3.start();}}

public class Ticket implements Runnable {//共100票int ticket = 100;@Overridepublic void run() {//模拟卖票while(true){if (ticket > 0) {//模拟选坐的操作try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);}}}

窗口1正在卖票:3

窗口2正在卖票:2

窗口3正在卖票:1

窗口2正在卖票:-1

窗口1正在卖票:0

运行结果发现:上面程序出现了问题

票出现了重复的票

错误的票 0、-1

解析:在上述代码while语句的if语句内,存在线程安全隐患,比如当前票数还剩下1张,窗口1、2、3正在抢票,若窗口1抢到了CPU的处理片,进入if语句内,进入try语句内,则程序进入休眠状态,窗口2或3其中之一抢到了CPU的处理片,再次进入if语句内,满足ticket > 0的条件,也再次进入了休眠状态,当窗口1过了sleep时间后,执行剩余操作,将ticket--,则ticket = 0,然后窗口2过了sleep时间后,再次执行剩余操作,将ticket--,则ticket = -1,所以出现了上述结果。Java提供了线程同步机制,能够解决线程安全问题。


线程安全的解决办法:

1. 线程同步

2. Lock接口

1.线程同步:

线程同步的方式有两种:

方式1:同步代码块

方式2:同步方法

方式一同步代码块:

在代码块声明时加上synchronized关键字。

synchronized(锁对象){    可能会产生线程安全问题的代码}

同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同有个锁对象才能保证线程安全。

实例:

public class Ticket implements Runnable {//共100票int ticket = 100;//定义锁对象Object lock = new Object();@Overridepublic void run() {//模拟卖票while(true){//同步代码块synchronized (lock){if (ticket > 0) {//模拟电影选坐的操作try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);}}}}}
在有线程任务的代码块中用synchronized(锁对象)来包裹起来。这样就可以解决线程安全问题,但前提是当多个线程同时使用一个线程任务时,必须保证锁对象唯一,不然仍然会出现安全问题。

方式二同步方法:

同步方法:在方法声明上加上synchronized关键字,可以在线程代码run方法中写一个用synchronized关键字修饰的函数,如:

public synchronized void method(){   可能会产生线程安全问题的代码}

同步方法中的锁对象是 this

public class Ticket implements Runnable {//共100票int ticket = 100;//定义锁对象Object lock = new Object();@Overridepublic void run() {//模拟卖票while(true){//同步方法method();}}//同步方法,锁对象thispublic synchronized void method(){if (ticket > 0) {//模拟选坐的操作try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);}}}

静态同步方法: 在方法声明上加上staticsynchronized

public static synchronized void method(){可能会产生线程安全问题的代码}

静态同步方法中的锁对象是类名.class

2.Lock接口:

Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。

void lock() 获取锁。

void unlock() 释放锁。

实例:更简单。

public class Ticket implements Runnable {//共100票int ticket = 100;//创建Lock锁对象Lock ck = new ReentrantLock();@Overridepublic void run() {//模拟卖票while(true){//synchronized (lock){ck.lock();if (ticket > 0) {//模拟选坐的操作try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);}ck.unlock();//}}}}


死锁:

同步锁使用的弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。

synchronzied(A锁){synchronized(B锁){           }}


等待唤醒机制:

在开始讲解等待唤醒机制之前,有必要搞清一个概念——线程之间的通信:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。通过一定的手段使各个线程能有效的利用资源。而这种手段即——等待唤醒机制

比如:实现一功能,input为一线程,output为一线程,main为主线程,实现input对成员变量赋值,output获取成员变量值。

但有一个问题是当input给一个成员变量赋值后,output开始获取成员变量值,此时还没有获取完全,紧接着input给下一个成员变量赋值,这就导致了output获取的值可能是input第二次给成员变量赋的值,要想解决此办法,必须让input和output这两个线程实现通信,保证一边input赋值后,等待output获取值,当其获取值后,给input一个信号,使其再赋值。才能保证不会出现乱值。

等待唤醒机制所涉及到的方法:

 wait() :等待,将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。

 notify():唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。

 notifyAll(): 唤醒全部:可以将线程池中的所有wait() 线程都唤醒。

1.当input发现Resource中没有数据时,开始输入,输入完成后,叫output来输出。如果发现有数据,就wait();

2.当output发现Resource中没有数据时,就wait() ;当发现有数据时,就输出,然后,叫醒input来输入数据。

public class Resource {private String name;private String sex;private boolean flag = false;public synchronized void set(String name, String sex) {if (flag)try {wait();} catch (InterruptedException e) {e.printStackTrace();}// 设置成员变量this.name = name;this.sex = sex;// 设置之后,Resource中有值,将标记该为 true ,flag = true;// 唤醒outputthis.notify();}public synchronized void out() {if (!flag)try {wait();} catch (InterruptedException e) {e.printStackTrace();}// 输出线程将数据输出System.out.println("姓名: " + name + ",性别: " + sex);// 改变标记,以便输入线程输入数据flag = false;// 唤醒input,进行数据输入this.notify();}}

public class Input implements Runnable {private Resource r;public Input(Resource r) {this.r = r;}@Overridepublic void run() {int count = 0;while (true) {if (count == 0) {r.set("小明", "男生");} else {r.set("小花", "女生");}// 在两个数据之间进行切换count = (count + 1) % 2;}}}

public class Output implements Runnable {private Resource r;public Output(Resource r) {this.r = r;}@Overridepublic void run() {while (true) {r.out();}}}

public class ResourceDemo {public static void main(String[] args) {// 资源对象Resource r = new Resource();// 任务对象Input in = new Input(r);Output out = new Output(r);// 线程对象Thread t1 = new Thread(in);Thread t2 = new Thread(out);// 开启线程t1.start();t2.start();}}


线程池

线程池概念:

线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复的使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

我们详细的解释一下为什么要使用线程池?

在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。

线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。

使用线程池方式:

1. Runnable接口

2. Callable接口

方式一:

实例:

import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;/* *  JDK1.5新特性,实现线程池程序步骤: *  1.使用工厂类 Executors中的静态方法创建线程对象,指定线程的个数 *   static ExecutorService newFixedThreadPool(int 个数) 返回线程池对象 *   返回的是ExecutorService接口的实现类 (线程池对象) *  2.创建Runnable类的对象,作为submit方法中的参数 *   接口实现类对象,调用方法submit (Ruunable r) 提交线程执行任务 *           */public class ThreadPoolDemo {public static void main(String[] args) {//调用工厂类的静态方法,创建线程池对象//返回线程池对象,是返回的接口ExecutorService es = Executors.newFixedThreadPool(2);    //调用接口实现类对象es中的方法submit提交线程任务//将Runnable接口实现类对象,传递es.submit(new ThreadPoolRunnable());//采用匿名对象es.submit(new ThreadPoolRunnable());es.submit(new ThreadPoolRunnable());}}


public class ThreadPoolRunnable implements Runnable {public void run(){System.out.println(Thread.currentThread().getName()+" 线程提交任务");}}

方式二:

实例:

import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Future;/* *  实现线程程序的第三个方式,实现Callable接口方式 *  实现步骤 *    1.工厂类 Executors静态方法newFixedThreadPool方法,创建线程池对象 *    线程池对象ExecutorService接口实现类,调用方法submit提交线程任务 *    2.创建Callable接口实现类的对象作为submit的参数 submit(Callable c) */public class ThreadPoolDemo1 {public static void main(String[] args)throws Exception {ExecutorService es = Executors.newFixedThreadPool(2);//提交线程任务的方法submit方法返回 Future接口的实现类Future<String> f = es.submit(new ThreadPoolCallable());String s = f.get();System.out.println(s);}}


import java.util.concurrent.Callable;public class ThreadPoolCallable implements Callable<String>{public String call(){return "abc";}}

采用此方法的优点在于:

Callable接口返回结果并且可能抛出异常的任务,该接口类中的方法call(),可以返回任意类型数据,会抛出异常,但是Runnable接口类中存放任务的方法run(),返回类型为void类型。建议使用第二种方式。