Java-多线程、线程池

多线程

线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少一个线程(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 { //继承Thread
MyThread(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方法,完成该线程执行的逻辑
*/
@Override
public 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方法逻辑
@Override
public 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;

@Override
public 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();
@Override
public 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();
@Override
public void run() {
//模拟卖票
while(true){
//同步方法
method();
}
}

//同步方法,锁对象this
public 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();

@Override
public 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;
// 唤醒output
this.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;
}

@Override
public 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;
}

@Override
public 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类型。建议使用第二种方式。

---------------------
作者:xiaoyuxianshenging
来源:CSDN
原文:https://blog.csdn.net/xiaoyuxianshenging/article/details/74276317
版权声明:本文为博主原创文章,转载请附上博文链接!

posted @ 2019-01-07 13:21  lemaden  阅读(72)  评论(0编辑  收藏  举报