多线程
目录
一、基本概念:程序、进程、线程
二、线程的创建和使用
三、线程的生命周期
四、线程的同步
五、线程的通信
六、JDK 5.0新增的线程创建方式
一、基本概念:程序、进程、线程
1、程序(program),是为完成特定任务、用某种语言编写的一组指令的集合。即++指一段静态的代码++,静态对象。
2、进程(process),是程序的一次执行过程,或是++正在运行的一个程序++。是一个动态的过程:有它自身的产生、存在和消亡的过程。 ——生命周期
- 如:运行中的QQ,运行中的MP3播放器
- 程序是静态的,进程是动态的
- ++进程作为资源分配的单位++, 系统在运行时会为每个进程分配不同的内存区域
3、线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。
- 若一个进程同一时间并行执行多个线程,就是支持多线程的
- ++线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc)++,线程切换的开销小
- 一个进程中的多个线程共享相同的内存单元/内存地址空间它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来++安全的隐患。++
4、并行与并发
- 并行: 多个CPU同时执行多个任务。比如:多个人同时做不同的事。
- 并发: 一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。
二、线程的创建和使用
方式一
1、创建一个继承于Thread类的子类
2、重写Thread类的run()方法:将此线程要执行的操作声明在润方法里
3、创建Thread类的子类的的对象
4、通过此对象调用start()方法①启用当前线程②调用当前线程
5、例子:遍历100以内的所有偶数
public class ThreadTest {
public static void main(String[] args) {
//3、创建Thread类的子类的的对象
MyThread t1 = new MyThread();
//4、通过此对象调用start()方法
t1.start();
for (int i = 0; i < 100; i++) {
if(i%2==0){
System.out.println("***");
System.out.println(Thread.currentThread().getName())//看是哪个线程
}
}//穿插出现
}
}
//1、创建一个继承于Thread类的子类
class MyThread extends Thread{
//2、重写Thread类的run()方法:将此线程要执行的操作声明在润方法里
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if(i%2==0){
System.out.println(i);
System.out.println(Thread.currentThread().getName())//看是哪个线程
}
}
}
}
6、两个问题
- 我们不能通过直接调用run()的方式启动线程->run()在主线程中
- 不可以让同一个对象再次调用Start()方法,会报IllegalThreadStartException异常->重新建一个对象
方式二:创建Thread子类的匿名对象
//方法二:创建Thread子类的匿名对象
new Thread(){
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if(i%2==0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}.start();
方式三:实现Runnable
1、创建一个实现了Runnable类的接口
2、在实现类中重写Runnable中的抽象方法:run()
3、创建实现类的对象
4、将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
5、通过Thread类的对象调用start()①启动线程②调用当前线程的run()方法–>调用了Runnable类中的target的run()方法
public class WindowTest {
public static void main(String[] args) {
//例子:创建三个窗口卖票,总票数为100张
Window window = new Window();
Thread w1 = new Thread(window);
Thread w2 = new Thread(window);
Thread w3 = new Thread(window);
w1.setName("窗口一");
w2.setName("窗口二");
w3.setName("窗口三");
w1.start();
w2.start();
w3.start();
}
}
class Window implements Runnable{
private int ticket = 100;
@Override
public void run() {
while(true){
if(ticket > 1){
System.out.println(Thread.currentThread().getName()+"你的票号为:"+ticket);
ticket--;
}else {
System.out.println("没票了");
break;
}
}
}
}
比较方式一和方式三
开发中:优先选择,实现Runnable接口的方式
原因:
- 1、实现的方式没有类的单继承的局限性。
- 2、实现的方式更适合来处理多个线程有共享数据的情况。
联系:
- 1、public class Thread implements Runnable
- 2、相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
方式四 实现Callable
Thread类中的方法
- void start(): 启动线程,并执行对象的run()方法
- run(): 线程在被调度时执行的操作,通常需要重写Thread类中的此方法,将创建的线程要执行的方阿飞声明在此方法中
- String getName(): 返回线程的名称
- void setName(String name):设置该线程名称
- 也可以用构造器设置名字,注意要在子类中重写构造器
- static Thread currentThread(): 返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类
- void yield():释放当前cpu的执行权
- void join():在线程a中调用线程b的join()方法,此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态
- void sleep():暂时阻塞当前线程 n 毫秒,++不会释放锁++
- boolean isAlive():判断当前线程是否还存货
- void stop():已过时,强制结束线程
线程的调度
线程的优先级
- MAX_PRIORITY:10
- NORM_PRIORITY:5 ——>默认优先级
- MIN_PRIORITY:1
如何设置当前线程的优先级
- getPriority():获取线程的优先级
- setPriority(int p):设置线程的优先级
- 优先级高只是在统计概率上的,高概率被执行,并不意味着等高优先级的执行完,低优先级才执行。
三、线程的生命周期
四、线程的同步
例子:创建三个窗口卖票,总票数为100张
- 1、问题:卖票过程中,出现了重票、错票
- 2、问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票
- 3、如何解决:但一个线程 a 操作共享数据(ticket)时,其他线程不能参与进来,直到线程 a 操作完共享数据(ticket)时,其他线程才开始操作 共享数据(ticket)。这种情况,即使线程 a 出现了阻塞,也不能被改变。
- 在Java中我们通过同步机制,来解决线程的安全问题
方式一:同步代码块
- 说明:
- ① 操作共享数据的代码即为需要被同步的代码。–>不能多了,也不能少了
- ② 共享数据:多个线程共同操作的变量,比如:ticket就是共享数据
- ③ 同步监视器,俗称:锁。任何一个类的对象都可以来充当锁,注:只能由一个锁(所以要在run()方法外创建对象创建对象)
- 好处:同步代码块方式,解决了线程的安全问题
- 局限性:操作同步代码时只能由一个线程参与,其他线程等待,相当于是一个单线程的过程,效率低。
synchronized(同步监视器){
需要被监视的代码
}
买票窗口 最终答案1(实现Runnable版)
-
注意:可以考虑用synchronized(this)来充当同步监视器
public class WindowTest {
public static void main(String[] args) {
//例子:创建三个窗口卖票,总票数为100张
Window window = new Window();
Thread w1 = new Thread(window);
Thread w2 = new Thread(window);
Thread w3 = new Thread(window);
w1.setName("窗口一");
w2.setName("窗口二");
w3.setName("窗口三");
w1.start();
w2.start();
w3.start();
}
}
class Window implements Runnable{
private int ticket = 100;
Object obj = new Object();//对象要放在外面,这样才是唯一的锁
@Override
public void run() {
while(true){
synchronized (obj) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() +"," +"你的票号为:" + ticket);
ticket--;
} else {
System.out.println("没票了");
break;
}
}
}
}
}
买票窗口 最终答案2(实现Thread版)
- 在声明Object(锁)的时候用 static 修饰
- 慎用synchronized(this)来充当同步监视器,this在(继承Thread版)代表三个对象
private static int ticket = 100;
private static Object obj = new Object();
- 可以用synchronized(Window.class),用当前的类充当同步监视器 --> 类也是对象,++而且类只会加载一次++
synchronized (this)
方式二:同步方法
说明:
- 如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步的
- 因为synchronized不能包多,也不能包少,所以间接地创建一个show()方法来存放要循环的代码块
总结:
- 同步方法中仍然涉及到同步监视器,只是不需要我们显式的去声明。
- 非静态的同步方法,同步监视器是:this
- 静态的同步方法,同步监视器是:当前类本身(Window.class)
1、实现Runnable版
public class WindowTest3 {
public static void main(String[] args) {
Window3 window3 = new Window3();
Thread w1 = new Thread(window3);
Thread w2 = new Thread(window3);
Thread w3 = new Thread(window3);
w1.setName("窗口 1");
w2.setName("窗口 2");
w3.setName("窗口 3");
w1.start();
w2.start();
w3.start();
}
}
class Window3 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while(true){
show();
if(ticket == 0){
break;
}
}
}
public synchronized void show(){
if (ticket > 0) {
try {
Thread.sleep(35);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +"," +"你的票号为:" + ticket);
ticket--;
}else{
System.out.println("没票了");
}
}
}
2、继承Thread版
- 用static修饰show()方法
- 注:getName()要改成Thread.currentThread.getName()
- 原因:getName()不是静态的(可以调用多次),但类是静态的(只调用一次)–>用类来调用getName()
public static synchronized void show()//同步监视器:Window.class
线程安全的单例模式之懒汉式
用同步机制将懒汉式改写为线程安全的
public class BankTest {
}
class Bank{
private Bank(){}
private static Bank instance = null;
public static Bank getInstance(){
//方式一:效率低
/*
synchronized (Bank.class){
if(instance == null){
instance = new Bank();
}
return instance;
}
*/
//方法二:效率高
if(instance == null){
synchronized (Bank.class){
if(instance == null){
instance = new Bank();
}
}
}
return instance;
}
}
演示线程的死锁问题
1、死锁的理解:不同的线程分别蝉蛹对方需要的同步资源不放弃,都在等待对方放弃自己寻妖的同步资源,就形成了死锁
2、说明①出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态无法继续②我们是同同步时,要比避免出现死锁
Lock(锁)
解决线程安全问题方式三(JDK 5.0 新增)
* 1、实例化ReentrantLock
* 2、把需要同步的代码块用 try-finally 包起来
* 3、在 try 开头调用锁定的方法 luck();
* 4、在 finally 里调用解锁的方法 unlock();
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window implements Runnable{
private int ticket = 100;
private ReentrantLock lock = new ReentrantLock(true);
@Override
public void run() {
while(true){
try {
//调用lock()方法:锁定
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",你的票号为:" + ticket);
ticket--;
} else {
break;
}
}finally {
//3、调用unlock()方法:解锁
lock.unlock();
}
}
}
}
面试题1:synchronized和Lock的方式有什么异同
异:
- 1、synhronized机制在执行完行医的同步代码块后,自动的释放同步监视器
- 2、Lock需要手动的启动同步(lock()),同时结束同步也需要手动地解锁(unlock())
- 3、在开发中一般用Lock,更灵活
练习
题目
两个人分别向一个账户存4000块钱,一次存1000,存完后打印余额
答案
public class BankTest2 {
public static void main(String[] args) {
Bank bank = new Bank(0);
Customer customer = new Customer(bank);
Thread c1 = new Thread(customer);
Thread c2 = new Thread(customer);
c1.setName("甲,");
c2.setName("乙,");
c2.start();
c1.start();
}
}
class Customer implements Runnable{
private Bank bank;
private ReentrantLock lock = new ReentrantLock(true);
public Customer(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
lock.lock();
bank.deposit();
lock.unlock();
}
}
}
class Bank {
private double balance = 0;
public Bank(double balance) {
this.balance = balance;
}
public void deposit(){
balance+=1000;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"存钱成功,余额为:"+balance);
}
}
五、线程的通信
说明:
- 1、这三个方法只能出现在同步代码块和同步方法中(Lock不行)
- 2、这三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则,会出现IllegalMonitorSatateException
- 3、这三个方法不是定义在在Thread中的,是定义在java.lang.Object类中的。
- 4、推导:调用者是同步监视器,任何一个类都可以充当同步监视器–>Object类–>着三个方法定义在Object类中
两个线程交替打印1-100的数
public class CommunicationTest {
public static void main(String[] args) {
Print print = new Print();
Thread t1 = new Thread(print);
Thread t2 = new Thread(print);
t1.start();
t2.start();
}
}
class Print implements Runnable{
private int num = 1;
@Override
public void run() {
while (true){
synchronized (this) {
notify();
System.out.println(Thread.currentThread().getName() + num);
num++;
if (num == 100) {
break;
}
try {
//使用wait()方法使线程进入阻塞状态
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
面试题:sleep()和wait()方法的异同
相同点:
- 一旦执行方法都可以是当前的线程i将纳入阻塞状态。
不同点:
- 两个方法声明的位置不同:① sleep()方法声明在Thread类中。 ②wait()方法声明在Object类中
- 调用的要求不同:①sleep()方法可以在任何需要的场景下调用 ②wait()必须在同步代码块和同步方法中调用
- 关于是否释放同步监视器的问题:①sleep()不会释放锁,wait()会释放锁。
六、JDK 5.0新增的线程创建方式
方式一:实现Callable接口
与使用Runnable相比, Callable功能更强大些
- 在Callable的实现类中,重写call()方法;相比run()方法,可以有返回值;
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask类,比如获取返回结果
Future接口
- 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
- FutrueTask是Futrue接口的唯一的实现类
- FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
步骤
- 1、创建一个实现Callable的实现类
- 2、实现call方法,将此线程需要执行的操作声明在call()中
- 3、创建Callable接口实现类的对象
- 4、将此Callable接口实现类的对象传递到FutureTask的构造器中,创建FutureTask的对象
- 5、将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread的对象,并调用start()方法,开始线程
- 6、获取Callable中call()方法的返回值(不想要返回值可以return null)
public class ThreadTest3 {
public static void main(String[] args) {
NewThread newThread = new NewThread();
FutureTask futureTask = new FutureTask(newThread);
new Thread(futureTask).start();
try {
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值
Object sum = futureTask.get();//获取返回值
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class NewThread implements Callable {
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if(i%2==0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
实现Callable接口 vs 实现 Runnable接口
- 1、call()可以有返回值
- 2、call()可以抛出异常,被外面的操作捕获,可以通过异常排除程序的错误
方式二:使用线程池
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8RmKYeLn-1610982285045)(1622AC572DEC431ABC93EFFD6C0B5060)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z4cWtc5R-1610982285046)(79571F1A678D45F3A917A81FE9A248C3)]
步骤
- 1、提供指定线程量的像城池
- 2、执行指定的线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象
- 3、关闭线程池
使用线程池的好处
- 1、提高响应速度(减少了创建新线程的时间)
- 2、降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
- 3、便于线程管理
public class ThreadPool {
public static void main(String[] args) {
//1、提供指定线程量的像城池
ExecutorService service = Executors.newFixedThreadPool(10);
//设置线程池的属性
System.out.println(service.getClass());//java.util.concurrent.ThreadPoolExecutor
ThreadPoolExecutor service2 = (ThreadPoolExecutor) service;//强转
service2.setCorePoolSize(10);//设置线程个数
//service2.setKeepAliveTime(); //设置活跃时间
//2、执行指定的线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumThread1());//适用于Runnable
service.submit(new NumThread2());//适用于Callable
//3、关闭线程池
service.shutdown();
}
}
class NumThread1 implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if(i%2==0) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
class NumThread2 implements Callable {
@Override
public Object call() throws Exception {
for (int i = 1; i <= 100 ; i++) {
if(i%2 != 0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
return null;
}
}