1、昨日复习
谈谈你对程序、进程、线程的理解
代码完成继承Thread的方式创建分线程,并遍历100以内的自然数
代码完成实现Runnable接口的方法创建分线程,并遍历100以内的自然数
对比两种创建方式
说说你对IDEA中Project和Module的理解
workspace project
project module
package package
2、线程的生命周期
3、线程的同步
问题的提出:
多个线程执行的不确定性引起执行结果的不稳定
多个线程对账本的共享,会造成操作的不完整性,会破坏数据。
1、问题:买票过程中出现了,重票、错票———->出现了线程的安全问题。
2、原因:当某个线程操作中,尚未完成时,其他线程参与进来,也操作车票。
3、如何解决:当一个线程在操作票时,其他线程不能参与进来,直到线程a操作完,其他线程才可以开始操作票,即使线程a出现了阻塞,也不能改变。
4、java中,通过同步机制,来解决线程的安全问题。
方式一:同步代码块
synchronized(同步监视器){
需要被同步的代码
}
说明:1、操作共享数据的代码,即为需要被同步的代码。—>不能包含代码多了,也不能少了。
2、共享数据:多个线程共同操作的变量。比如:票的数量。
3、同步监视器:锁。任何类的对象都可以充当锁。该对象不能是NULL值。
要求:多个线程必须共用同一把锁
补充:在实现Runnable接口创建多线程的方式中,可以考虑使用this充当同步监视器。
在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器
方式二:同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步的。
1、同步方法任然涉及到同步监视器,只是不需要我们显示的声明。
2、非静态的同步方法,同步监视器默认是:this
静态的同步方法,同步监视器默认是:当前类本身。
5、同步的方式,解决了线程的安全问题。——>好处。
操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程,效率低。——>局限性
1、死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
2、说明:
(1)出现死锁后,不会出现异常,不会出现报错,只是所有的线程都处于阻塞状态,无法继续
(2)我们使用同步时,要避免出现死锁。
解决方法 :
专门的算法、原则
尽量减少同步资源的定义
尽量避免嵌套同步
方式三:Lock锁——-jdk5.0新增
package com.atguigu.java3;
/*
银行有一个账户。
有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打
印账户余额。
*/
public class AccountTest {
public static void main(String[] args) {
Account acct = new Account(0);
Customer c1 = new Customer(acct);
Customer c2 = new Customer(acct);
c1.setName("甲");
c2.setName("乙");
c1.start();
c2.start();
}
}
class Account {
private double balance;
public Account(double balance) {
this.balance = balance;
}
public synchronized void depoist(double amt){
if (amt>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance+=amt;
System.out.println(Thread.currentThread().getName()+":存钱成功,余额为:"+this.balance);
}
}
}
class Customer extends Thread{
private Account account;
public Customer(Account account) {
this.account = account;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
account.depoist(1000);
}
}
}
4、线程的通信
涉及到的三个方法:
wait():一旦执行该方法,当前线程就进入阻塞状态,并释放同步监视器。
notify():一旦执行该方法,就会唤醒wait的一个线程,如果有多个wait的线程,就唤醒优先级高的
notifyAll():一旦执行该方法,就会唤醒所有的被wait的线程。
说明:
1、三个方法必须使用在同步代码块或同步方法中。
2、这三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则会出异常。
3、这三个方法是定义在Object类中的
面试题:sleep()和wait()的异同
1、相同点:一旦执行方法,都可以使得当前线程进入阻塞状态
2、不同点:(1)两个方法声明的位置不同:Thread类中声明sleep(),Object()类中声明wait()
(2)调用的范围不同:sleep()可以在任何需要的场景下调用,wait()必须使用在同步代码块 或同步方法中。
(3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep不会释放 锁,wait会释放锁。
package com.atguigu.java4;
/*
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处
取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图
生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通
知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如
果店中有产品了再通知消费者来取走产品。
分析:
1、是否是多线程?是
2、是否有共享数据?是
3、如何解决线程的安全?三种
4、是否涉及到线程的通信?是
*/
public class ProductTest {
public static void main(String[] args) {
Clerk clerk= new Clerk();
Productor p1 = new Productor(clerk);
p1.setName("生产者1");
Customer c1 = new Customer(clerk);
c1.setName("消费者1");
Customer c2 = new Customer(clerk);
c2.setName("消费者2");
p1.start();
c1.start();
c2.start();
}
}
class Clerk {
private int productCount=0;
public synchronized void produceProduct() {//this
if (productCount<20){
productCount++;
System.out.println(Thread.currentThread().getName()+":开始生产第"+productCount+"个产品");
notify();
}else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void consumeProduct() {//this
if (productCount>0){
System.out.println(Thread.currentThread().getName()+":开始消费第"+productCount+"个产品");
productCount--;
notify();
}else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Productor extends Thread {
private Clerk clerk;
public Productor(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName()+":开始生产产品。。。");
while (true){
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produceProduct();
}
}
}
class Customer extends Thread {
private Clerk clerk;
public Customer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName()+":开始消费产品。。。");
while (true){
try {
sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumeProduct();
}
}
}
5、jdk新增线程创建方式
新增方式一:实现Callable接口
与使用Runnable相比, Callable功能更强大些
相比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方法的返回值,调用FutureTask的对象.get();方法。
如何理解实现Callable接口的方法创建多线程比实现Runnable接口创建多线程方式强大?
1、call()可以有返回值
2、call()可以抛出异常,被外面的操作捕获,获取异常的信息
3、call()是支持泛型的
新增方式二:使用线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
好处:
提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止
1、提供指定线程数量的线程池。
2、执行指定的线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象。
3、关闭线程池。