多线程学习思路:为什么学习线程?为了解决CPU利用率问题,提高CPU利用率。 =》 什么是进程?什么是线程? =》 怎么创建线程?有哪几种方式?有什么特点? =》 分别怎么启动线程? =》 多线程带来了数据安全问题,该怎么解决? =》 怎么使用synchronized(同步)决解? =》使用同步可能会产生死锁,该怎么决解? =》 线程之间是如何通信的? =》 线程有返回值吗?该如何拿到? =》 怎么才能一次性启动几百上千个的线程?
什么是进程
进程是操作系统中正在执行的不同的应用程序,例如:我们可以同时打开Word和记事本
什么是线程
线程是一个应用程序进程中不同的执行路径,例如:我们的WEB服务器,能够为多个用户同时提供请求服务
进程是不活泼的。进程从来不执行任何东西,它只是线程的容器。线程总是在某个进程环境中创建的,而且它的整个寿命期都在该进程中。
– Java的多线程
• Java 中的多线程是通过java.lang.Thread类来实现的.
• 一个Java应用程序java.exe,其实至少有三个线程: main()主线程, gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
• 使用多线程的优点。
– 背景:以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短(因为单线程的可以减少cup的调度消耗的时间),为何仍需多线程呢?
– 多线程程序的优点:
1.提高应用程序的响应。对图形化界面更有意义,可增强用户体验。同时做多个事情。比如:一边听歌、一边写代码。
2.提高计算机系统CPU的利用率。
3.改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
• 何时需要多线程
– 程序需要同时执行两个或多个任务。
– 需要一些后台运行的程序时。比如:Java后台运行的GC功能。
主线程
– 概念
• 即使Java程序没有显示的来声明一个线程,Java也会有一个线程存在该线程叫做主线程
• 可以调用Thread.currentThread()来获得当前线程
有两种方法来创建线程
• 继承Thread类
– MyThread extends Thread
» 需要覆盖run方法
• 实现Runnable接口
– Runnable 中有一个方法run用来定义线程执行代码
– public void run();
后面还会介绍两种,一共是四种创建方式。
线程的启动
• 线程的启动需要调用Thread的start方法,不能直接调用run方法,如果直接调用run方法相当于方法调用。
线程的终止
• 当run方法返回,线程终止,一旦线程终止后不能再次启动。
• 线程的终止可以调用线程的interrupt方法,但该方法不是最佳方法,最好是设置一个标记来控制线程的终止。
注意事项:一个线程只能启动一次,不能多次启动同一个线程。
线程控制基本方法
• Thread类的有关方法(1)
– void start():启动线程并执行对象的run()方法
– run():线程在被调度时执行的操作
– String getName():返回线程的名称
– void setName(String name):设置该线程名称
– static Thread currentThread():返回当前线程。
• 线程控制的基本方法
线程的优先级
– 线程的优先级越高占用CPU时间越长
– 最高为10级,最低为1级,默认是5级
线程的状态转换
线程创建的选择
• 创建线程的两种方式。
开发中:优先选择实现Runnable 接口的方式来创建线程。
– 1.实现接口的方式没有类的单继承性的局限性。
– 2.实现接口的方式更适合来处理多个线程有共享数据的情况。
线程的练习
创建三个窗口卖票,总票数为100张。(通过线程的两种实现方式分别来完成)
方式一:
//练习,100张票三人卖
public class WindowTest2 implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket <= 0) {
return;
}
System.out.println(Thread.currentThread().getName() + "-->售出第:" + (101 - ticket) + "票");
ticket--;
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
WindowTest2 wt1 = new WindowTest2();
//创建三个线程,并启动,通过的是同一个对象来创建,所以票数可以是非静态的
new Thread(wt1).start();
new Thread(wt1).start();
new Thread(wt1).start();
//线程安全问题:同一个时间,多个线程访问(修改)同一个对象,造成结果不可预测(混乱)
//线程安全问题的条件:1.同一时间、2.多个线程一起访问、3操作的是同一个对象
/*输出的部分结果:
* Thread-1-->售出第:1票
* Thread-0-->售出第:1票
* Thread-2-->售出第:1票
* Thread-1-->售出第:4票
* Thread-2-->售出第:4票
* Thread-0-->售出第:4票
* Thread-1-->售出第:7票
* Thread-2-->售出第:7票
* Thread-0-->售出第:7票
*/
}
}
方式二:
//练习,100张票三人卖
public class WindowTest1 extends Thread {
private static int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket <= 0) {
return;
}
getTicket();
}
}
private static void getTicket() {
if(ticket==0) return;
System.out.println(Thread.currentThread().getName() + "-->售出第:" + (101 - ticket) + "票");
ticket--;
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
WindowTest1 wt1 = new WindowTest1();
WindowTest1 wt2 = new WindowTest1();
WindowTest1 wt3 = new WindowTest1();
wt1.start();
wt2.start();
wt3.start();
//线程安全问题:同一个时间,多个线程访问(修改)同一个对象,造成结果不可预测(混乱)
//线程安全问题的条件:1.同一时间、2.多个线程一起访问、3操作的是同一个对象
/*输出的部分结果:
* Thread-1-->售出第:1票
* Thread-0-->售出第:1票
* Thread-2-->售出第:1票
* Thread-1-->售出第:4票
* Thread-2-->售出第:4票
* Thread-0-->售出第:4票
* Thread-1-->售出第:7票
* Thread-2-->售出第:7票
* Thread-0-->售出第:7票
*/
}
}
在这里买票的三个窗口,会出现买了同一张票的问题,后面将会决解这个问题。
• 为什么需要线程同步:一个银行账号在同一时间不能接受多个线程的访问,因为这样会造成混乱
• 线程的同步
synchronized
线程安全问题:
1 同一时间
2 多个线程
3 操作同一个账号
就会出现混乱情况,这种由于多线程引发的混乱情况,我们就称他为:线程安全问题
如何解决呢?同步操作了解决线程问题。
synchronized关键字
• synchronized 是Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
1、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
2、然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
3、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
4、第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
• 方式一:同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
说明:
1. 操作共享数据的代码,即为需要被同步的代码
2.共享数据:多个线程共同操作的变量。比如: ticket 就是共享数据。
3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
通过第一种同步方式修改售票问题
//练习,100张票三人卖
public class WindowTest4 implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
// 同步关键字括号中是同步监视器
if (ticket <= 0) {
return;
}
getTicket();
}
}
private synchronized void getTicket() {
if (ticket == 0)
return;
System.out.println(Thread.currentThread().getName() + "-->售出第:" + (101 - ticket) + "票");
ticket--;
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
WindowTest4 wt1 = new WindowTest4();
// 创建三个线程,并启动,通过的是同一个对象来创建,所以票数可以是非静态的
new Thread(wt1).start();
new Thread(wt1).start();
new Thread(wt1).start();
/*部分输出结果:
* Thread-1-->售出第:1票
* Thread-1-->售出第:2票
* Thread-1-->售出第:3票
* Thread-1-->售出第:4票
* Thread-2-->售出第:5票
* Thread-2-->售出第:6票
* Thread-2-->售出第:7票
* Thread-0-->售出第:8票
* Thread-2-->售出第:9票
* Thread-2-->售出第:10票
* Thread-1-->售出第:11票
* Thread-1-->售出第:12票
*/
}
}
单例模式:懒汉模式的同步问题及解决
/*
* 单例模式:懒汉模式
*/
public class UserManager {
// 懒汉模式:用的时候才创建,不用的时候为null
private static UserManager instance;
private int id;
private String name;
private UserManager() {
}
// 方式一:给方法加入synchronized关键字
// public static synchronized UserManager getInstance() {
//
// if(instance==null) {
// instance = new UserManager();
// }
// return instance;
// }
// 方式二:通过同步代码块的方式实现线程安全问题
// public static UserManager getInstance() {
//
// synchronized (UserManager.class) {
// if (instance == null) {
// instance = new UserManager();
// }
// }
// return instance;
// }
// 方式三:通过双检测机制实现对象的创建,更安全,效率更高
public static UserManager getInstance() {
if (instance == null) {
synchronized (UserManager.class) {
if (instance == null) {
instance = new UserManager();
}
}
}
return instance;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
线程同步带来的问题:死锁
理解什么是死锁?
死锁问题的产生:
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
解决方法
➢通过逻辑算法来避免出现死锁。
➢尽量减少同步资源的定义。
➢尽量避免嵌套同步。
• wait/notify/notifyAll
– wait():执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
– notify():执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的。
– notifyAll():执行此方法,就会唤醒所有被wait的线程。
• 说明:
经典案例:生成者、消费者问题
public class ProducerConsumer {
public static void main(String[] args) {
BaoziStack baoziStack = new BaoziStack();
Producer p1 = new Producer(baoziStack);
Consumer c1 = new Consumer(baoziStack);
p1.start();
c1.start();
}
}
// 包子类
class Baozi {
int id;
public Baozi(int id) {
this.id = id;
}
@Override
public String toString() {
return "包子 : " + id;
}
}
// 包子筐
class BaoziStack {
Baozi[] bz = new Baozi[10];
int index = 0;
// 装包子
public synchronized void pushBZ(Baozi baozi) {
if (index >= bz.length) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
bz[index] = baozi;
index++;
notify();
}
// 取包子
public synchronized Baozi popBZ() {
if (index <= 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
index--;
Baozi baozi = bz[index];
notify();
return baozi;
}
}
// 生产者:生产包子,放到包子筐里
class Producer extends Thread {
private BaoziStack baoziStack;
public Producer(BaoziStack baoziStack) {
this.baoziStack = baoziStack;
}
@Override
public void run() {
// 生产包子(一天生产100个包子)
for (int i = 1; i <= 100; i++) {
Baozi baozi = new Baozi(i);
System.out.println("生产者生产了一个包子ID为: " + i);
baoziStack.pushBZ(baozi);
try {
sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 消费者
class Consumer extends Thread {
private BaoziStack baoziStack;
public Consumer(BaoziStack baoziStack) {
this.baoziStack = baoziStack;
}
@Override
public void run() {
// 一天的消费量为100个包子
for (int i = 1; i <= 100; i++) {
Baozi baozi = baoziStack.popBZ();
System.out.println("消费者消费了一个包子ID为:" + baozi.id + "的包子");
try {
sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
JDK5.0新增方式一:实现Callable接口
Runnable和Callable的区别:
1、Callable规定的方法是call(),Runnable规定的方法是run().
2、Callable的任务执行后可返回值,而Runnable的任务是不能有返回值。
3、call方法可以抛出异常,run方法不可以。
DK5.0新增方式二:使用线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
JDK5.0起提供了线程池相关API: ExecutorService 和Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
void execute(Runnable command):执行任务1命令,没有返回值,-般用来执行Runnable
< T > Future< T > submit(Callable< T > task):执行任务, 有返回值,一般来执行Callable
void shutdown():关闭连接池
Executors: 工具类、线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool(): 创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor(): 创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n): 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
好处:
1.提高响应速度(减少创建新线程的时间)
2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
3.便于线程管理
corePoolSize:核心池的大小
maximumPoolsize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止
线程池实例如下
newFixedThreadPool() 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
感谢阅览~~~
看只是学习的一种输入途径而已,重要的是理解、实践和输出。输入和输出要保持好唷~~不然只有输入没有多上输出很快就会把学过的知识忘记了ヽ(*。>Д<)o゜