一、线程执行的内存原理
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t2.start();
}
对应的内存原理图大致是这样:
注意事项:
- 执行线程任务的run方法是线程私有的。
- 某个线程对象出现异常不会影响其他线程的执行。
二、创建线程的方式
(1)继承Thread类
1、步骤
- 定义一个类,继承Thread类。
- 重写Thread类的
run
方法。 - 创建线程对象。
- 调用
start
方法开启新线程,内部会执行run
方法。
2、代码示例
public class MyThread extends Thread {
@Override
public void run() {
// 获取线程名称
String threadName = this.getName();
for (int i=0;i<100;i++) {
// 复习异常抛出的方法,抛出一个运行时异常
if("Thread-1".equals(threadName) && i == 3){
throw new RuntimeException(threadName + "出问题了");
}
System.out.println(threadName+"..."+i);
}
}
}
3、Thread类的构造函数
public Thread()
:分配一个新的线程对象。public Thread(String name):
分配一个指定名字的新的线程对象。public Thread(Runnable target)
:分配一个带有指定目标新的线程对象。public Thread(Runnable target,String name)
:分配一个带有指定目标新的线程对象并指定名字。
4、常用成员方法
public String getName():
获取当前线程名称。public String start();
导致此线程开始执行;java虚拟机调用此线程的run
方法。public void run():
定义线程的任务逻辑。
5、常用静态方法
public static void sleep(long millis)
: 让当前正在执行的进程暂停指定毫秒数。public static Thread currentThread()
:返回对当前正在执行的线程对象的引用。
(2)实现Runnable接口
1、步骤
- 定义Runnable接口的实现类
- 实现类覆盖重写
run
方法,指定线程任务逻辑 - 创建Runnable接口实现类对象
- 创建Thread类对象,构造函数传递Runnable实践类对象。
a) public Thread(Runnable target):分配一个带有指定目标新的线程对象。
b) public Thread(Runnable target,String name):分配一个带有指定目标新的线程对象并指定名字。
- Thread类对象调用
start
方法,内部自动调用run
方法。
2、代码展示
public class MyTask implements Runnable{
@Override
public void run() {
//任务逻辑
}
}
MyTask myTask = new MyTask();
Thread thread = new Thread(myTask);
thread.start();
2、(重点!)实现Runnable接口来创建线程的好处
- 避免java中类单继承的局限性
- 降低线程任务对象和线程之间的耦合性
tip:换句话说,我们可以更加专注于线程的任务,先把线程的任务逻辑创建完毕。之后需要执行线程任务的地方就创建线程,执行需要的线程任务即可。并且线程任务可以多次反复使用。有点像零件插拔一样。
(3)匿名内部类方式
1、格式
new 父类/接口(){
//覆盖重写抽象方法
};
2、作用
- 创建父类子类对象的快捷方式
- 创建接口的实现类对象的快捷方式
3、注意事项
- 使用匿名内部类创建的对象只能一次性使用
- 尽量使用lambda表达式进行书写,提高代码可读性和编程效率。
三、(重点!)线程安全问题
(1)出现线程安全的情况
-
有两个以上线程同时操作共享数据
-
操作共享数据的语句有两条以上
-
线程调度是抢占式调度模式。
(2)解决方案
-
同一个线程,操作共享数据的多条语句全部执行,要么多条语句全部不执行。故而可以使用同步技术。
-
同步的原理:有锁的线程执行,没有锁的线程等待。
(3)实际解决
1、同步代码块
-
作用:用来解决多线程访问共享数据安全问题
-
格式
synchronized(任意对象){
}
- 注意事项:
(1)所有操作共享数据的代码写到同步代码块{}中。
(2)任意对象:任意指的是类型可以任意,但要保证全局唯一,被多个线程共享使用。- 任意对象,也叫锁对象。更加专业的术语:对象监视器。
2、同步方法
- 格式
修饰符 synchronized 返回值类型 方法名称(参数列表...){
...
}
- 注意事项
(1)所有操作共享数据的代码都在{}中间添加一个
(2)同步方法的锁对象就是this
3、使用Lock接口
-
方法:
abstract void lock()
获得锁。
abstract void unlock()
释放锁。 -
实现类
java.util.concurrent.locks.ReentrantLock ,空参构造函数 -
注意事项:
释放锁的动作必须被执行。
(4)实际案例
1、卖票案例分析
(1)总共有3种途径卖票,每个途径,相当于一个线程对象
(2)每个线程对象要执行的任务: 都是在卖票
(3)3个线程对象,操作的资源 100 张票 是被共享的
2、解决策略:
(1) 定义实现类,实现Runnable接口
(2) 覆盖重写Runnable接口中的run方法.指定线程任务——卖票
(2.1)判断是否有票
(2.2)有: 出一张票
(2.3)票的数量减少1
(3) 创建Runnable接口的实现类对象
(4) 创建3个Thread对象,传递Runnable接口的实现类对象,代表,卖票的3种途径
(5) 3个Thread对象分别调用start方法,开启售票
3、代码实现
public class MyTicket implements Runnable{
private int tickets= 100;
Object obj = new Object();
@Override
public void run() {
while (true){
// sellTicketB();
sellTicketA();
}
}
// 同步函数
private synchronized void sellTicketA(){
if(tickets>0){
System.out.println(Thread.currentThread().getName() + " 卖出第" + tickets-- + "张票");
}else {
return;
}
}
//同步进程快
private void sellTicketB() {
synchronized(obj){
if(tickets>0){
System.out.println(Thread.currentThread().getName() + " 卖出第" + tickets-- + "张票");
}else {
return;
}
}
}
}
public class synchronizedTest {
public static void main(String[] args) {
MyTicket task = new MyTicket();
// 三个线程任务来出票
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
}
}
4、线程同步的原理
-
线程执行的前提:
(1)cpu资源
(2)锁对象 -
基本规则:
线程对象执行同步代码块中的内容,要么全部执行,要么全部不执行,不能够被其他线程干扰。 -
拿买票案例举例说明
现在存在t0、t1和t2三个线程。
假设一:
假设t0线程获取cpu资源,执行线程任务遇到同步代码块,判断是否具有锁对象。
有:获取锁对象
进入同步代码块,执行同步代码,,假设t0在执行过程中没有被t1或者t2抢夺cpu资源,那么t0或顺利执行完同步代码块内代码,退出同步代码块,释放锁资源,继续和其他线程抢夺cpu资源和锁对象。
假设二:
假设t0线程获取cpu资源,执行线程任务遇到同步代码块,判断是否具有锁对象
有:获取锁对象
进入同步代码块,执行同步代码,假设t0在执行过程中被t1抢夺了cpu资源,那么t0线程将不能继续执行。t1线程执行任务,遇到同步代码块,判断是否具有锁对象,因为锁已经被t0拿了,因此t1进入阻塞状态,等待获取锁对象被释放。
假设三
假设t0执行完成了同步代码块的内容,释放了锁对象,t1处于阻塞状态,但此时t2线程抢到了cpu资源,执行代码到同步代码块,然后顺利获取锁对象,进入同步代码块执行。这种情况下,t1将继续等待t2在同步代码块执行完毕,然后再去抢夺cpu资源和锁资源。
可以发现,线程如果不进行调度的管理可能会出现长时间等待的问题,因为抢占式调度具有随机性,不能获得最大的性能。
(持续更新…)
四、线程状态
java.lang.Thread.State给出了六种
线程状态
线程状态 | 导致状态发生条件 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动,还没有调用start方法 |
Runnable(可运行) | 线程可以再java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于cpu |
Blocked(锁阻塞) | 当一个线程试图获取一个独享锁,而该对象被其他的线程持有,则该线程进入Blocked状态;当该对象持有锁时,该线程将变成Runnable状态 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
Timed Waiting(计时等待 | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。 |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
注意事项(一)
sleep
方法可以在同步中使用sleep
方法可以在非同步中使用sleep
方法与锁对象无关(不会释放锁)
注意事项(二)
Object类
定义wait
和notify
方法- 因此任意对象可以调用
wait()
和notify()
方法 - 锁对象可以是任意的
- 但是锁对象必须使用在同步中,因此
wait
和notify
方法必须在同步中使用
案例分析
双线程交替执行。有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为
{10,5,20,50,100,200,500,800,2,80,300,3000};
创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2”,随机从抽奖池中完成抽奖。
两个线程轮流交替抽奖,每抽出一个奖项就打印出来。
【输出示例】
抽奖箱1...抽出了10元...
抽奖箱2...抽出了20元...
抽奖箱1...抽出了50元...
抽奖箱2...抽出了800元...
... ...
每次抽的过程中,不打印,抽完时一次性打印。
【输出示例】
在此次抽奖过程中,抽奖箱1总共产生了6个奖项,分别为:10,5,20,50,100,200最高奖项为200元,总计额为385元
在此次抽奖过程中,抽奖箱2总共产生了6个奖项,分别为:500,800,2,80,300,3000最高奖项为3000元,总计额为4682元
在此次抽奖过程中,抽奖项2中产生了最高奖项,该最高奖项为3000元
1. 分析
两个线程的任务都是抽奖,因此很明显只需要定义一个线程任务对象“抽奖”即可。由于两个抽奖箱共享一个奖池,且要求两个抽奖箱交替进行,很明显需要用到线程的等待(wait)和唤醒(notify)操作。因此,两个线程对于奖池中奖金的操作需要同步。前面已经说明,线程任务只有一个,故同步代码块的锁对象使用线程任务对象自身(this)即可。
2、实现思路
3、代码实现
public class RunnableImpl implements Runnable{
private List list;
private Map<String,List> mp = new HashMap<>();
private int count = 0;
public RunnableImpl(List list) {
this.list = list;
}
@Override
public void run() {
List subList = new ArrayList();
while (true){
synchronized (this){
if(list.size()<=0){
this.notifyAll();
mp.put(Thread.currentThread().getName(),subList);
count++;
if(count == 2){
Integer max = 0;
String max_name = "";
for (Map.Entry<String, List> entry : mp.entrySet()) {
List t = entry.getValue();
String s = entry.getKey();
Integer tmax = 0;
Integer sum = 0;
StringBuilder sb = new StringBuilder();
for (Object o : t) {
sb = sb.append(o).append(",");
sum += (Integer)o;
tmax = tmax < (Integer)o ? (Integer)o : tmax;
}
if(max < tmax){
max = tmax;
max_name = s;
}
//sb = sb.deleteCharAt(sb.length()-1);
String seq =sb.toString();
System.out.println("在此次抽奖过程中,"+ s +"总共产生了"+t.size()+"个奖项,分别为:" + seq +"最高奖项为"+ tmax +"元,总计额为"+ sum +"元");
}
System.out.println("在此次抽奖过程中,"+max_name+"中产生了最高奖项,该最高奖项为"+max+"元");
}
break;
}
if(list.size() > 0){
Object remove = list.remove(new Random().nextInt(list.size()));
System.out.println(Thread.currentThread().getName() + "..." + "抽出了"+ remove +"元...");
subList.add(remove);
this.notify();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
public class Test {
public static void main(String[] args) {
List<Integer> list = new ArrayList(Arrays.asList(10,5,20,50,100,200,500,800,2,80,300,3000));
RunnableImpl runnable = new RunnableImpl(list);
new Thread(runnable,"抽奖箱1").start();
new Thread(runnable,"抽奖箱2").start();
}
}
五、生产者消费者模式
案例分析
包子铺卖包子,吃货吃饱做,包子铺包一个包子,吃货吃一个包子。
1、图解分析
2、多个线程间的通信
多个不同任务的线程要操作共享数据,一定需要统一的协调,也就是说需要一个统一的锁对象。在这种情况下,不能直接使用线程自己(this)作为锁对象,因为线程任务不同,必然也是各自独立的。因此,这个场景下,选择了盘子(List对象)来作为锁,因为他是全局唯一个,和生产者与消费者都有着密切关系。
3、代码实现
生产者
/*
包子铺:
1.判断盘子中有没有包子
2.无:生产一个包子,直到装满盘子,唤醒等待锁的线程
3.有:释放锁
*/
public class Productor implements Runnable{
private List list;
private int count = 0;
public Productor(List list) {
this.list = list;
}
@Override
public void run() {
while (true){
synchronized (list){
try {
//盘子有了包子,进入等待
if(list.size() > 0){
list.wait();
}
//没有进入等待说明盘子空了,开始制作包子
String bz ="皮多肉韭菜鸡蛋包子";
list.add(bz);
System.out.println(Thread.currentThread().getName() + " 包了" + bz + count++);
// 唤醒吃货吃包子
list.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
消费者
/*
吃货:
1.看盘子里有没有包子
2.有:吃掉
3.无:释放锁,唤醒正在等待锁的包子铺接着做包子
*/
public class Consumer implements Runnable{
private List list;
private int count=0;
public Consumer(List list) {
this.list = list;
}
@Override
public void run() {
while (true) {
synchronized (list) {
// 盘子中没有包子了,进入等待阶段
if(list.size() == 0){
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 没有进入等待说明盘子里还有包子
while (list.size()>0){
String bz = (String)list.remove(0) + count++;
System.out.println(Thread.currentThread().getName() + "吃了" + bz);
}
// 包子被吃完了,唤醒袍子铺做包子
list.notify();
}
}
}
}
public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Productor productor = new Productor(list);
Consumer consumer = new Consumer(list);
Thread t0 = new Thread(productor);
t0.setName("庆丰包子铺");
t0.start();
Thread t1 = new Thread(consumer);
t1.setName("吃货A");
t1.start();
Thread t2 = new Thread(consumer);
t2.setName("吃货B");
t2.start();
}
}
转载:https://blog.csdn.net/weixin_38708854/article/details/106957789