Java多线程
多线程
概述
-
多线程:
- 进程:正在执行中的程序,其实是应用程序在内存中运行的那片空间。
- 线程:进程中的一个执行单元,负责进程中的程序的运行,一个进程中至少要有一个线程。
- 多线程程序:一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
- 程序启动了多线程,有什么应用呢?可以实现多部分程序同时执行,专业术语:“并发”
好处与弊端
多线程好处:解决了多部分同时运行的问题。
多线程的弊端:其实应用程序的执行都是cpu
在做着快速的切换完成的。这个切换是随机的。线程太多随之而来的是效率的降低。
其实应用程序的执行都是cpu
在做着快速的切换完成的。这个切换是随机的。
JVM
中的多线程解析
其实应用程序的执行都是cpu
在做着快速 的切换完成的。这个切换是随机的。
对于JVM
,启动时,就启动了多个线程,至少有两个线程可以分析的出来:
-
jvm
的主线程执行main函数的线程。 该线程的任务代码都定义在main函数的中。
-
jvm
负责垃圾回收的线程。系统垃圾回收的的方法:Object类中的finalize()
当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。子类重写 finalize 方法,以配置系统资源或执行其他清除
系统中启动垃圾回收的方法是
System.gc()
;
一个进程至少有一个线程在运行,当一个进程中出现多个线程时,就称这个应用程序是多线程应用程序,每个线程在栈区中都有自己的执行空间,自己的方法区、自己的变量。
jvm
在启动的时,首先有一个主线程,负责程序的执行,调用的是main函数。主线程执行的代码都在main方法中。
当产生垃圾时,收垃圾的动作,是不需要主线程来完成,因为这样,会出现主线程中的代码执行会停止,会去运行垃圾回收器代码,效率较低,所以由单独一个线程来负责垃圾回收。
如何创建一个线程呢?
创建线程方式一:继承Thread类。``
创建线程的第二种方式:实现Runnable接口。[跳转](# 第二种方式实现Runnable接口:)(ps
:按ctrl
键点击跳转)
内存与线程名称获取
-
内存:
- 多线程执行时,在栈内存中,每一个执行线程都有一片自己所属的栈内存空间,进行方法的压栈和弹栈。
- 当执行线程的任务结束了,线程自动就在栈内存中释放了。
- 当所有的执行线程都结束时,进程就结束了。
-
获取线程名称:
- 获取当前线程对象:
Thread: currentThread()
- 获取名称:
getName();
- 故:
Thread.currentThread().getName();
- 主线程的名称:main
- 自定义的线程:Thread-1 线程多个时,数字顺延。Thread-n,n从0开始
- 获取当前线程对象:
多线程的创建一:继承Thread类
继承Thread类步骤:
- 定义一个类继承Thread
- 重写Thread类中的run方法
- 直接创建Thread的子类对象创建线程
- 调用start方法,开启线程并让线程执行,同时还会告诉
jvm
去调用run方法。
继承Thread类的原理
基础 文学:::
继承Thread类:
- 因为Thread类描述线程事物,具备线程应有的功能。
- 那为什么不直接创建Thread类的对象呢?
Thread t1 = new Thread();
t1.start();
- 这么做没有错,但该start调用的是Thread类中的run方法
- 而这个run方法没有做什么事情,更重要的是,这个run方法中没有定义我们需要让线程执行的代码。
创建线程的目的是什么?
- 为了建立单独的执行路径,让多部分代码实现同时执行。
- 也就是说,线程创建并执行需要给定的代码(专业术语:线程的任务)。
- 对于之前所讲的主线程,它的任务定义在了main函数中。
- 自定义的线程需要执行的任务都定义在run方法中。
- Thread类中的run方法内部的任务并不是我们所需要的,只要重写run方法,
- 既然Thread类已经定义了线程任务的位置,只要在位置中定义任务代码即可
- 所以进行了重写run方法的动作。
另一套说法:
jvm
创建的主线程的任务都定义在了主函数中。- 而自定义的线程它的人物在哪儿呢?
- Thread类中用于描述线程,线程是需要任务的。所以Thread类也对任务的描述。
- 这个任务就通过Thread类中的run方法来实现。也就是说,run方法就是封装自定义线程运行任务的函数。
- run方法中定义就是线程要运行的任务代码。
- 开启线程是为了运行指定代码,所以只有继承Thread,并复写run方法。
- 将运行的代码定义在run方法中即可。
/*
进程:是一个正在执行中的程序。
每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元。
线程:就是进程中的一个独立的控制单元。
线程在控制着进程的执行。
一个进程中至少有一个线程。
Java VM 启动的时候会有一个进程java.exe.
该进程中至少一个线程负责java程序的执行。
而且这个线程运行的代码存在于main方法中。
该线程称之为主线程。
扩展:其实更细节说明jvm,jvm启动不止一个线程,还有负责垃圾回收机制的线程。
1,如何在自定义的代码中,自定义一个线程呢?
通过对api的查找,java已经提供了对线程这类事物的描述。就Thread类。
创建线程的第一种方式:继承Thread类。
步骤:
1,定义类继承Thread。
2,复写Thread类中的run方法。
目的:将自定义代码存储在run方法。让线程运行。
3,调用线程的start方法,
该方法两个作用:启动线程,调用run方法。
发现运行结果每一次都不同。
因为多个线程都获取cpu的执行权。cpu执行到谁,谁就运行。
明确一点,在某一个时刻,只能有一个程序在运行。(多核除外)
cpu在做着快速的切换,以达到看上去是同时运行的效果。
我们可以形象把多线程的运行行为在互相抢夺cpu的执行权。
这就是多线程的一个特性:随机性。谁抢到谁执行,至于执行多长,cpu说的算。
为什么要覆盖run方法呢?
Thread类用于描述线程。
该类就定义了一个功能,用于存储线程要运行的代码。该存储功能就是run方法。
也就是说Thread类中的run方法,用于存储线程要运行的代码。
*/
class Demo extends Thread
{
public void run()
{
for(int x=0; x<60; x++)
System.out.println("demo run----"+x);
}
}
class ThreadDemo
{
public static void main(String[] args)
{
//for(int x=0; x<4000; x++)
//System.out.println("Hello World!");
Demo d = new Demo();//创建好一个线程。
//d.start();//开启线程并执行该线程的run方法。
d.run();//仅仅是对象调用方法。而线程创建了,并没有运行。
for(int x=0; x<60; x++)
System.out.println("Hello World!--"+x);
}
}
/*
练习:
创建两个线程,和主线程交替运行。
*/
代码实现
多线程(Thread类中的方法&线程名称)
/*
如何创建一个线程呢?
创建线程方式一:继承Thread类。
步骤:
1,定义一个类继承Thread类。
2,重写Thread类中的run方法。
3,直接创建Thread的子类对象创建线程。
4,调用start方法,开启线程并让线程执行,同时还会告诉jvm去调用run方法。
为什么要这么做?
获取线程名称:
Thread:currentThread()获取当前线程对象。怎么名称呢?getName();
Thread.currentThread().getName();
继承Thread类:因为Thread类描述线程事物,具备线程应该有功能。
可以通过Thread的getName获取线程的名称 Thread-编号(从0开始)
主线程的名字就是main。
*/
class Demo extends Thread
{
private String name;
Demo(String name)
{
super(name);
//this.name = name;
}
public void run()
{
for(int x=0; x<10; x++)
{
//for(int y=-9999999; y<999999999; y++){}
System.out.println(name+"....x="+x+".....name="+Thread.currentThread().getName());
}
}
}
class ThreadDemo2
{
public static void main(String[] args)
{
/*
创建线程的目的是为了开启一条执行路径,去运行指定的代码和其他代码实现同时运行。
而运行的指定代码就是这个执行路径的任务。
jvm创建的主线程的任务都定义在了主函数中。
而自定义的线程它的任务在哪儿呢?
Thread类用于描述线程,线程是需要任务的。所以Thread类也对任务的描述。
这个任务就通过Thread类中的run方法来体现。也就是说,run方法就是封装自定义线程运行任务的函数。
run方法中定义就是线程要运行的任务代码。
开启线程是为了运行指定代码,所以只有继承Thread类,并复写run方法。
将运行的代码定义在run方法中即可。
*/
// Thread t1 = new Thread();
Demo d1 = new Demo("旺财");
Demo d2 = new Demo("小强");
d1.start();//开启线程,调用run方法。
d2.start();
System.out.println("over...."+Thread.currentThread().getName());
}
}
/*
面试题:调用run和调用start有什么区别?
调用run方法不开启线程。仅是对象调用方法。
调用start开启线程,并让jvm调用run方法在开启的线程中执行。
*/
调用run和调用start有什么区别?
- 调用run方法不开启线程。仅是对象调用方法。
- 调用start开启线程,并让
jvm
调用run方法在开启的线程中执行。
上面这个程序的结果是:一会输出旺财.....0,一会输出小强...0因为我们启动了两个线程(实际上是三个),这两个线程是都是由cpu
在切换执行,所以也才会出现这种情况
多线程运行图
他们三个线程互不影响,各自有各自的所属,如果其中一个出现了异常,那么并不会影响其他两个异常,其他两个还会照样执行
多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。
进行方法的压栈和弹栈。
当执行线程的任务结束了,线程自动在栈内存中释放了。
但是所有的执行线程都结束了,进程就结束了。
多线程(线程的状态)
多线程技术的一些特点:
有一个并发的特点,而且出现了一个随机性,是因为cpu
不断的切换造成的
创建并不代表运行,你必须要start一次过后,它才会有资格去运行
多线程的创建二:实现Runnable接口
API
接口Runnablepublic interface Runnable
Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为run的无参数方法。
然后可以分配该类的实例,在创建Thread时作为一个参数来传递并启动。
创建线程的第一种方式:继承Thread类。
创建线程的第二种方式:实现Runnable接口。
步骤与理解:
-
定义类实现Runnable接口。
-
覆盖接口中的run方法,将线程的任务代码封装到run方法中。
-
创建Thread类的对象,只有创建Thread类的对象才可以创建线程。
-
将Runnable接口的子类对象作为参数传递给Thread类的构造函数。
为什么?
-
因为线程的任务已被封装在Runnable接口子类对象的run方法中。 而这个run方法所属于Runnable接口的子类对象,
-
所以将这个子类对象作为参数传递給Thread的构造函数,这样,线程对象创建时就可以明确要运行的线程的任务。
- 调用线程对象的start方法开启线程。
通过源码的形式讲解一下将runnable接口的子类对象作为参数传递给Thread构造函数的原因。
// jdk 源码 class Thread{ private Runnable target; Thread(Runnable target){ this.target = target; } public void run(){ if(target != null){ target.run(); } } public void start(){ run(); } // 具体实现 class Demo implements Runnable{ private String name; Demo(String name){ this.name = name; } //覆盖了接口Runnable中的run方法。 public void run(){ for(int x = 1;x <= 20; x++){ System.out.println("name="+name+"..."+Thread.currentThread().getName()+"..."+x); } } } 主函数: Runnable d = new Demo(); Thread t = new Thread(d); t.start(); }
第二种方式实现Runnable接口:
- 第二种方式实现Runnable接口避免了单继承的局限性,所以较为常用。
- 实现Runnable接口的方式,更加的符合面向对象,线程分为两部分,一部分线程对象,一部分线程任务。
- 继承Thread类:线程对象和线程任务耦合在一起。一旦创建Thread接口的子类对象,即使线程对象,又有线程任务。
- 实现Runnable接口,将线程任务单独分离出来封装成对象。类型就是Runnable接口类型。
- Runnable接口对线程对象和线程任务进行解耦。
- 用Runnable来标明线程任务,用Thread来明确线程对象。
实现Runnable接口的好处:
-
将线程的任务从线程的子类中分离出来,进行了单独的封装。
按照面向对象的思想将任务的封装成对象。
-
避免了
java
单继承的局限性。
所以,创建线程的第二种方式较为常用。
class Demo implements Runnable //extends Fu
//准备扩展Demo类的功能,让其中的内容可以作为线程的任务执行。
//通过接口的形式完成。
class Demo implements Runnable{
private String name;
Demo(String name){
this.name = name;
}
// 覆盖了接口Runnable中的run方法。
}
public void run()
{
for(int x=0; x<20; x++)
{
System.out.println(Thread.currentThread().getName()+"....."+x);
}
}
}
class ThreadDemo
{
public static void main(String[] args)
{
// 创建Runnable子类的对象。注意它并不是线程对象。
Demo d = new Demo("confront");
//创建Thread类的对象,将Runnable接口的子类对象作为参数传递给Thread类的构造函数。
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
// 将线程启动。
t1.start(); // Thread-0 启动
t2.start(); // Thread-1 启动
System.out.println(Thread.currentThread().getName+"------->"); //主线程启动
// Demo d1 = new Demo();
// Demo d2 = new Demo();
// d1.start();
// d2.start();
}
}
多线程(第二种方式的细节)
class Thread
{
{
private Runnable r;
Thread()
{
}
Thread(Runnable r)
{
this.r = r;
}
public void run()
{
if(r!=null)
r.run();
}
public void start()
{
run();
}
}
class ThreadImpl implements Runnable
{
public void run()
{
System.out.println("runnablerun");
}
}
ThreadImpl i = newThreadImpl();
Thread t = new Thread(i);
t.start();
class SubThread extendsThread
{
public void run()
{
System.out.println("hahah");
}
}
//SubThread s = newSubThread();
//s.start();
案例——售票程序
class Ticket implements Runnable//extends Thread
{
//1.描述票的数量
private int tick = 100;
//2.售票的动作,这个动作需要被多编程执行,那就是线程任务代码。需要定义run方法中。
//线程任务重通常都有循环结构。
public void run()
{
while(true)
{
if(tick>0)
{
System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
//创建Runnable接口的子类对象
Ticket t = new Ticket();
// 创建四个线程对象。并将Runnable接口的子类对象作为参数传递给Thread的构造函数。
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
/*
Ticket t1 = new Ticket();
//Ticket t2 = new Ticket();
//Ticket t3 = new Ticket();
//Ticket t4 = new Ticket();
t1.start();
t1.start();
t1.start();
t1.start();
*/
//下面输出的结果是Thread---0.Thread---1,Thread----2.Thread-----3
//都卖了100张票,为什么会现这个问题呢,下面图解解答困惑
}
}
这里面本来可以使用staticint num
= 100来解决问题的也,用数据共享,但是为什么不这样做也,有可能现在这四个线程卖100张票,另外四个线程麦另外100张票,很有可能现在2个100张票啊,那如果是8个是不是100张啊
再有另外一种解决方法
这个运行会抛出异常:信息
原因分析:
//一个线程不能开启两次,会抛出无效线程状态异常
因为一开启,已经具备了运行资格,或者具备阻塞资格,你已经开启了为什么还要开启也
都发生异常了,你后面怎么还在执行也,,这个吧,你得先弄清楚这个里面有几个线程
说说这上面这个注释的有几个线程啊,2个吧
我一开始想错了,我以为是5个也,什么创建线程也Ticket t1 = new Ticket();这个才叫做创建线程,t1.start();
只是开启吧,只是开启四次而已,你就以为有四个路径了是吧,你这是什么选手也
这个异常发生异常了,是不是在主线程发生的啊,怎么看呢,上面这个有一个38行,这里面再一调t1.start()
;是不是发生异常了,你是不是抛到主线程中来了啊,主线程调用的吗,对吧,可不是它出事了吧,而t1的代码在哪儿呢是不是在publicvoid run()
,在38行发生的异常是不是主线程的代码啊,主线程代码发生异常当然是在主线程上了,一般异常包含四部分信息,一是包含所属的线程,二是异常名称,三是异常信息,四是异常的位置
现在解决问题:为什么不继承,用实现Runnable接口都能解决问题也:我也没有听明白,就说了,我只封装线程任务,封装完过后,我只封装资源就是这个票的数据
t2.start();
t3.start();
t4.start();
解释:
Ticket t = new Ticket();
//创建一个线程任务对象
Thread t1 = new Thread(t);
//创建线程,线程一初始化是不是有任务(t)
多线程的安全问题
发生现象
上面这个小程序存在着安全隐患。见图:
简单说明一下:最后num
数值减到为1了,然后线程0去判断num
.>0,然后也执行到这个里面了,因为cpu
执行是随机的吗,这个时候也还没有执行到输出语句,没有减1这个时候也就去执行线程1去了,判断满足吧,然后线程2又去判断,满足,然后线程三去判断满足,这个时候也,0,1,2,3都执行到了,if这里面来了,这个时候再去执行输出语句,执行是不是就有问题了也,是的
为了能看到这个问题也:下面也我们这个线程也睡10毫秒,这里面有一个知识点,有时候这个异常信息只能trycatch不能抛,为什么呢,因为父类的run的方法没有抛,所以我们覆盖的时候也不能抛,这就是为什么不能抛的原因,只能处理异常,下面就是上面出现例外的情况
异常代码:
class Ticket implements Runnable//extends Thread
{
private int tick = 100;
public void run()
{
while(true)
{
if(tick>0)
{
System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
通过分析,发现,打印出0,-1,-2等错票。
多线程的运行出现了安全问题。
问题的原因:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,
另一个线程参与进来执行。导致共享数据的错误。
解决办法:
对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行。
产生的原因
线程安全问题产生的原因:
-
多个线程在操作共享的数据。
-
操作共享数据的线程代码有多条(运算有多个)。
当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算。
就会导致线程安全问题的产生。
以后想要分析多线程程序是否有安全问题:
不能光看运行结果,因为运行一万次可能都是正常的,得看上面的情况可没可能发生。
同步代码块
解决思路:
就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,
其他线程时不可以参与运算的。
必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。
在java
中,用同步代码块就可以解决这个问题。(synchronized同步的意思)
同步代码块的格式:
synchronized(对象)
{
需要被同步的代码 ;
}
对象如同锁。持有锁的线程可以在同步中执行。
没有持有锁的线程即使获取cpu
的执行权,也进不去,因为没有获取锁。
class Ticket implements Runnable
{
private int tick = 1000;
Object obj = new Object();
public void run()
{
while(true)
{
synchronized(obj)
{
if(tick>0)
{
//try{Thread.sleep(10);}catch(InterruptException e){}
//InterruptException 中断异常
System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
}
}
}
}
}
class TicketDemo2
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
同步的好处和弊端
你觉得呢 你意思吃饭 对 那你还不收拾东西我没有东西 我的东西你不收拾? 不是我的爱整理的乖老婆?你没手 可是我有老婆你觉得呢 嗯?
这个obj就相当于一个对象的锁,当有人进来 的时候就加锁,当出去的时候也就解锁就完了,这就是线程同步解决安全隐患的原因,就是多个锁
火车上的卫生间---经典。
同步的好处:解决了线程的安全问题。
同步的弊端:相对降低了效率,因为同步外的线程的都会判断同步锁。多个线程需要判断锁,较为消耗资源。
同步的前提
现在的问题来了,我变换一下代码:
为什么还会出现这个问题也,为什么把Object obj = new Object();放在num=100下面就没事呢,放在run里面就有事呢
分析:原因在这儿每个线程开启过后是不是都有自己的run方法,每个run方法里面是不是都有一个局部变量obj对象,意味着每个线程都有自己的锁,一旦有了自己的锁过后也,就不再同步了,一个同步代码块里面只有一个线程在运行,我有四个,因为我用的是四把锁,那为什么放在上面就没事呢,这个时候这个obj就变成了成员变量,是在Ticket
这个对象中,跟num
一样,这个obj在内存中是不是唯一的一个啊,如果你写在下面的话也是不是在堆内存中有四个啊
同步的前提:
- 必须要有两个或者两个以上的线程。
- 必须是多个线程使用同一个锁。
必须保证同步中只能有一个线程在运行。
同步的另一种体现形式 同步函数
接下来我们准备用一个事例,来把刚才我们说的那些同步内容呢,简单的去练习一下,怎么在一个事例中如果存在多线程的情况下去分析它的安全隐患在哪儿,该如何去解决:
问这一个例子有没有案例隐患:怎么去分析:
分析:
要想知道这个程序的安全隐患在哪儿,你必须要遵循刚才我们所讲的,造成线程安全隐患的原因,原因里边也涉及到这几个安全要素,在线程运行的代码中,是否有共享数据,这个b就是共享,多个线程是不是在操作同一个b啊,必须的吗,同一个银行吗,这涉及安全隐患不,不涉及,b.add(100);是不是一句话啊,,你得先搞清楚线程的代码有哪些,不就是run方法这些吗,这种认识啊太父潜了,这个b调用了add方法吗,是不是run方法进栈以后,这个线程调用add方法,是不是add方法也进栈了啊,是不是进的同一个线程栈,是的,add方法也属于线程的代码
因为它被我们所定义的线程运行到了就这么简单,sum是不是也是共享数据啊,第一个条件满足了,好的,对共享数据的操作是不是不止一条啊,,是的,一条加法,一条输出,,现在也假设啊,sum为100,然后执行到sum = sum+num,sum=100吧,下面也还没有等输出也,这个时候这个线程就挂这儿了,另外一个线程进来了是不是又传了100啊,是不是又加上了,这个时候就是200了,另外一个输出多少也是不是也是200啊,现在为了看到效果了,在sum=sum+num加上代码,运行起来看
这样是不是出问题了啊,怎么办,把它封装起来吧,加上封装代码:
你发现了没有sum =sum + num;这个是不是函数代码:是啊,你发现了没有,函数本身是一种封装吧是哈,它带大括号的吧,同步代码块是不是也是封装体,他们两个都是封装体,一个是封装,另外一个是带有特性的封装,我们现在让函数具备同步性是不是ok了啊
/*
需求:
银行有一个金库。
有两个储户分别存300员,每次存100,存3次。
目的:该程序是否有安全问题,如果有,如何解决?
如何找问题:
1,明确哪些代码是多线程运行代码。
2,明确共享数据。
3,明确多线程运行代码中哪些语句是操作共享数据的。
*/
class Bank
{
private int sum;
//Object obj = new Object();
public synchronized void add(int n)
{
//synchronized(obj)
//{
sum = sum + n;
try{Thread.sleep(10);}catch(Exception e){}
System.out.println("sum="+sum);
//}
}
}
class Cus implements Runnable
{
private Bank b = new Bank();
public void run()
{
for(int x=0; x<3; x++)
{
b.add(100);
}
}
}
class BankDemo
{
public static void main(String[] args)
{
Cus c = new Cus();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
如果我们把这个函数变成了同步以后,保证了我们的线程安全问题不再发生,这个对象没有用上吧,同步代码块是不是有锁啊,那到了同步函数的锁呢,同步函数的锁到底是谁呢,请听下面讲解
验证同步函数的锁
现在说一现象:见代码:
这里面是不是只有0号线程啊,这就不明白为什么1,2,3线程不出来,是不是谁抢到谁运行啊,不一定每次都是0吧,问题在哪儿呢,你不是加同步了吗,什么该同步,什么不该同步,你没有搞清楚,其实我们都知道该同步的是不是if上面这儿啊,你在run方法加同步意味着什么啊,这里面来了0线程,1线程,2线程,0线程只要一进run方法已经进同步里面来了啊,它只要不出去,谁也进不来,1 2 3 有没有获取资源,有没有执行权,0线程sleep了,是不是1,2,3线程有执行权了啊,是得到执行权了,只是进不来而已,0线程把票卖完了,结果其他线程也没有进来,0不出去啊,0这辈子也不出去了,为啥啊,while(true)这是不是无限的啊,就是票卖完了,它还在这里面转也,这种方式转变为同步函数是不是不靠谱啊,。因为这个while(true)根本就不需要同步
告诉我怎么封装同步函数啊,把这个代码拿出来用一个函数封装一下是不是就完了啊
见代码:
下面就用这个例了来说一下这个函数的同步锁是哪一个
同步函数使用的锁是this
同步函数和同步代码块的区别:
同步函数的锁是固定的this。
同步代码块的锁是任意的对象。
建议使用同步代码块
验证static同步函数的锁
当同步函数被static修饰时,这时的同步用的是哪个锁呢?
静态函数在加载时所属于类,这时有可能还没有该类产生的对象,但是该类的字节码文件加载进内存就已经被封装成了对象,这个对象就是该类的字节码文件对象。
所以静态加载时,只有一个对象存在,那么静态同步函数就使用的这个对象。
这个对象就是 类名.class
同步代码块和同步函数的区别?
同步代码块使用的锁可以是任意对象。
同步函数使用的锁是this,静态同步函数的锁是该类的字节码文件对象。
在一个类中只有一个同步,可以使用同步函数。如果有多同步,必须使用同步代码块,来确定不同的锁。所以同步代码块相对灵活一些。
/*
静态的同步函数是用的锁是 该函数所属字节码文件对象
可以用 getClass 方法获取,也可以用当前 类名.class表示。
如果同步函数被静态修饰后,使用的锁是什么呢?
通过验证,发现不在是this。因为静态方法中也不可以定义this。
静态进内存是,内存中没有本类对象,但是一定有该类对应的字节码文件对象。
类名.class 该对象的类型是Class
静态的同步方法,使用的锁是该方法所在类的字节码文件对象。 类名.class
*/
class Ticket implements Runnable
{
private static int tick = 100;
//Object obj = new Object();
boolean flag = true;
public void run()
{
if(flag)
{
//进true 用同步代码快方式
while(true)
{
synchronized(Ticket.class)//(this.getClass()) 非静态方法
{
if(tick>0)
{
try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"....code : "+ tick--);
}
}
}
}
else
//否则,用同步函数
while(true)
show();
}
public static synchronized void show()
{
if(tick>0)
{
try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"....show.... : "+ tick--);
}
}
}
class StaticMethodDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
//Class clazz = t.getClass(); 创建的Ticket对象,调用方法
//Class clazz = Ticket.class; 直接就可以获取到class属性
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try{Thread.sleep(10);}catch(Exception e){}
//第一个线程开始,睡10毫秒,让第二个线程用调用同步函数的方式。
t.flag = false;
t2.start();
}
}
单例模式涉及的多线程问题
单例懒汉式的并发访问(高频考点)
现在讲一个之前我们已经学过的知识点,这有一部分与多线程技术相关,没有说完,所以我们现在把这个知识点重新拿起来再说一次
/*
单例设计模式。
*/
//饿汉式。
/*
class Single
{
private static final Single s = new Single();
private Single(){}
public static Single getInstance()
{
return s;
}
}
*/
//懒汉式 (面试常用)
//可以用双重判断的形式来解决懒汉式的安全问题和效率问题
class Single
{
private static Single s = null;
private Single(){}
public static Single getInstance()
{
// 多加一次判断是为了解决效率问题
if(s==null)
{
//加锁是为了解决线程安全问题
synchronized(Single.class)
{
if(s==null)
//--->A;
s = new Single();
}
}
return s;
}
}
class SingleDemo
{
public static void main(String[] args)
{
System.out.println("Hello World!");
}
}
现在思考在多线程情况下有没有安全隐患
如果这个getInstance()
它要被加载到了run方法中,是不是意味着要被多线程所执行,多线程执行的时候这个里面涉及到了共享数据,s就是共享数据,存不存在安全问题呢,饿汉式是不是不存在啊,谁来了是不是直接返回啊,饿汉式是没有问题的
接下来看看懒汉式中,这个getInstance
放到了run方法中,就意味着可以被多线程所执行啊,一分析发现,如果这些if做为多线程运行的代码话也,在代码中有没有共享数据,有吧,s就是,有没有多条语句在操作这个s啊,有没有问题验证一下就知道了,一般的分析方法就是在第一条语句执行完过后,线程挂起了,然后另外一个线程进来了
我们先在函数上加synchroziced
,这样解决问题不,分析,就算是在if过后线程挂起了,其他的线程对象进不来,解决问题了,但是另外一个问题就是,所以每次拿这个对象都要判断锁,是不是效率低,为了提高效率,把代码改写一下,改成加同步代码块,
小知识点 :this==对象名.class
,
第一个if判断是解决效率问题,synchronized
是解决线程安全问题
★考点问题:请写一个延迟加载的单例模式?写懒汉式;当出现多线程访问时怎么解决?加同步,解决安全问题;效率高吗?不高;怎样解决?通过双重判断的形式解决。
//懒汉式:延迟加载方式。
当多线程访问懒汉式时,因为懒汉式的方法内对共性数据进行多条语句的操作。所以容易出现线程安全问题。为了解决,加入同步机制,解决安全问题。但是却带来了效率降低。
为了效率问题,通过双重判断的形式解决。
同步的另一个弊端:死锁
下面我们准备用一个程序来看看示例DeadLockDemo
简单点来说,就是你的有我的锁,我有你的锁
死锁:常见情景之一:同步的嵌套
现在我们写一个死锁程序
/*
死锁。
同步中嵌套同步。
*/
class Test implements Runnable
{
private boolean flag;
Test(boolean flag)
{
this.flag = flag;
}
public void run()
{
if(flag)
{
while(true)
{
synchronized(MyLock.locka)
{
System.out.println(Thread.currentThread().getName()+"...if locka ");
synchronized(MyLock.lockb)
{
System.out.println(Thread.currentThread().getName()+"..if lockb");
}
}
}
}
else
{
while(true)
{
synchronized(MyLock.lockb)
{
System.out.println(Thread.currentThread().getName()+"..else lockb");
synchronized(MyLock.locka)
{
System.out.println(Thread.currentThread().getName()+".....else locka");
}
}
}
}
}
}
class MyLock
{
static Object locka = new Object();
static Object lockb = new Object();
}
class DeadLockTest
{
public static void main(String[] args)
{
Thread t1 = new Thread(new Test(true));
Thread t2 = new Thread(new Test(false));
t1.start();
t2.start();
}
}
上面这个加while(true)是为了看到效果,其实可以不用加的,看运气了,加while(true)是为了多试几次
多线程间的通信
以前啊有一个共同特点就是,多个线程是不是在执行同一个动作,多个线程是不是执行同一个任务
多线程通讯生活中的示例就是例如拉媒,执行的是同一个资源吧,一个人往这里面放煤,另外一个在拉煤,是不是啊
这个我们怎么去描述呢
第一步该怎么做,这个资源里面的数据不唯一,那就封装成对象
第二步:那这个输入和输出,这是不是同一个动作啊,你扔一个我输出一个,他们是不是同时运行啊,这个时候就涉及到多线程技术了啊,那他们的任务是不是不一样啊,是不是就有两个run方法,要分别封装在两个类中,所以在这儿我都想到了三个类,一个是资源,一个是输入,一个是输出,
这里面的输出是不是打印就完事了啊,你打印一次不合适吧,我仍一个你输出一个扔一个输出一个,,是不是不断在做这件事情啊,那就来个while(true)后面就知道了,为什么要写这个无限
/*
线程间通讯:
多个线程在处理同一资源,但是任务却不同。
*/
class Resource
{
String name;
String sex;
}
//输入
class Input implements Runnable
{
Resource r ;
// Object obj = new Object();
Input(Resource r)
{
this.r = r;
}
public void run()
{
int x = 0;
while(true)
{
synchronized(r)
{
if(x==0)
{
r.name = "mike";
r.sex = "nan";
}
else
{
r.name = "丽丽";
r.sex = "女女女女女女";
}
}
x = (x+1)%2;
}
}
}
//输出
class Output implements Runnable
{
Resource r;
// Object obj = new Object();
Output(Resource r)
{
this.r = r;
}
public void run()
{
while(true)
{
synchronized(r)
{
System.out.println(r.name+"....."+r.sex);
}
}
}
}
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();
}
}
怎么全是女也,是因为吧,前一个线程吧赋完mike过后,这个输入线程还有cpu的执行权,然后就赋值为了丽丽,就把前面的数据给覆盖了
可不可实现我输入一个make男,你就输出一次make男,我输入一次丽丽 女就你输出一次女也,上面完整的代码:
线程间通信:思路:多个线程在操作同一个资源,但是操作的动作却不一样。
1:将资源封装成对象。
2:将线程执行的任务(任务其实就是run方法。)也封装成对象。
等待唤醒机制
你在输入的时候是不是要先判断一下,有没有数据啊,如果有你是不是覆盖了啊,这里就要使用到wait和notify来解决这个问题
这里面必须要明确是哪一个锁,就像操场是有两起小朋友在玩冰棍这种游戏一样,你那边wait了,我这边不能救吧
r.wait();
要有所属,如果你被wait了,你被wait在哪儿这个r的线程池中,如果调用了
等待唤醒机制:
注:对象监视器:锁
涉及的方法:
wait
:将同步中的线程处于冻结状态。释放了执行权,释放了资格,同时将被wait
线程对象临时存储到线程池中。
notify
:会唤醒线程池中任意一个等待的线程。立刻有执行资格,但不一定立刻有执行权
notifyAll
:会唤醒线程池中所有的等待线程。
注意:
-
这些方法必须使用在同步中,因为必须要标识wait,notify等方法所属的锁(对象监视器)。
-
因为这些方法必须要标示所属的锁。
A锁上的线程被wait了,那这个线程就相当于处于A锁的线程池中,只能A锁的notify唤醒。
-
这三个方法都定义在Object类中,为什么这些方法定义在Object类中呢?
- 因为这些方法必须标识所属的锁,而锁可以是任意对象。
- 能被任意对象调用的方法一定定义在Object类中。
- 举例:小朋友抓人游戏
<font color=#3ADF00代码:
/*
线程间通讯:
其实就是多个线程在操作同一个资源,
但是操作的动作不同。
*/
class Resource
{
private String name;
private String sex;
private boolean flag = false;
public synchronized void set(String name,String sex)
{
if(flag)
try{this.wait();}catch(InterruptedException e){}
this.name = name;
this.sex = sex;
flag = true;
this.notify();
}
public synchronized void out()
{
if(!flag)
try{this.wait();}catch(InterruptedException e){}
System.out.println(name+"........"+sex);
flag = false;
this.notify();
}
}
class Input implements Runnable
{
private Resource r ;
Input(Resource r)
{
this.r = r;
}
public void run()
{
int x = 0;
while(true)
{
if(x==0)
r.set("mike","man");
else
r.set("丽丽","女女女女女");
x = (x+1)%2;
}
}
}
class Output implements Runnable
{
private Resource r ;
Output(Resource r)
{
this.r = r;
}
public void run()
{
while(true)
{
r.out();
}
}
}
class InputOutputDemo2
{
public static void main(String[] args)
{
Resource r = new Resource();
new Thread(new Input(r)).start();
new Thread(new Output(r)).start();
/*
Input in = new Input(r);
Output out = new Output(r);
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
t1.start();
t2.start();
*/
}
}
生产者消费者问题
★★★考点问题:面试会考,划重点
- 面试会考,划重点
- 生产和消费同时执行,需要多线程。
- 但是执行的任务不同,处理的资源却相同 -- 线程间的通信。
- 代码框架:
- 1、描述资源
- 2、描述生产者,因为具备着自己的任务
- 3、描述消费者,因为具备着自己的任务
- 问题1:数据错误,已经被生产很早期的商品,才被消费到,出现了线程安全问题。加入同步解决。
- 使用同步函数。问题已解决,不会再消费到之前很早期的商品
- 问题2:发现了连续生产却没有消费,同时对同一个商品进行多次消费。
希望的结果应该是:生产一个商品,就被消费掉,再生产下一个商品。 - 搞清楚几个问题:
- 生产者什么时候生产?消费者什么时候消费?
- 当盘子中没有面包时,就生产;如果有了面包,就不要生产。
- 当盘子中已有面包时,就消费;如果没有面包,就不要消费。
- 思考:
- 生产者生产了商品后,应该告诉消费者来消费。这时的生产者应该处于等待状态。
- 消费者消费了商品后,应该告诉生产者来生产。这时的消费者应该处于等待状态。
- 等待:wait(); ---- 需要
InterruptedException
捕获异常 - 告诉:notify(); -- 唤醒
- 问题解决:实现了生产一个消费一个。
多生产者和多消费者问题
- 问题1:生成了商品没有被消费,同一个商品被消费多次。
- 被唤醒的线程没有判断标记,造成问题1产生
- 解决:只要让被唤醒的线程必须判断标记就可以了。
将if判断标记的方式改为while判断标记 - 记住:只要是多生产多消费,必须while判断
- 问题2:
while
判断后,死锁了。- 原因:生产方唤醒了线程池中的生产方的线程。本方唤醒了本方。
- 解决:希望本方要唤醒对方。没有对应方法,所以,唤醒所有。
- 仍然有一些小遗憾:效率低了。唤醒所有会有点多余。
多生产者和多消费者问题
class ProducerConsumerDemo
{
public static void main(String[] args)
{
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(pro);
Thread t3 = new Thread(con);
Thread t4 = new Thread(con);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
/*
对于多个生产者和消费者。
为什么要定义while判断标记。
原因:让被唤醒的线程再一次判断标记。
为什么定义notifyAll,
因为需要唤醒对方线程。
因为只用notify,容易出现只唤醒本方线程的情况。导致程序中的所有线程都等待。
*/
class Resource
{
private String name;
private int count = 1;
private boolean flag = false;
// t1 t2
public synchronized void set(String name)
{
while(flag)
try{this.wait();}catch(Exception e){}//t1(放弃资格) t2(获取资格)
this.name = name+"--"+count++;
System.out.println(Thread.currentThread().getName()+"...生产者.."+this.name);
flag = true;
this.notifyAll();
}
// t3 t4
public synchronized void out()
{
while(!flag)
try{wait();}catch(Exception e){}//t3(放弃资格) t4(放弃资格)
System.out.println(Thread.currentThread().getName()+"...消费者........."+this.name);
flag = false;
this.notifyAll();
}
}
class Producer implements Runnable
{
private Resource res;
Producer(Resource res)
{
this.res = res;
}
public void run()
{
while(true)
{
res.set("+商品+");
}
}
}
class Consumer implements Runnable
{
private Resource res;
Consumer(Resource res)
{
this.res = res;
}
public void run()
{
while(true)
{
res.out();
}
}
}
多生产多消费JDK1.5
解决办法
JDK1.5Lock
接口
obj锁与Lock锁的区别图:
- 提供了多生产多消费的解决方案
- 在
java.util.concurrent.locks
软件包中提供相应的解决方案 - Lock接口:比同步更厉害,有更多的操作。lock(),-- 获取锁 unlock();-- 释放锁
- 提供了一个更加面向对象的锁,在该锁中,提供了更多的显式的锁操作。
- 可以替代同步。 - 先把同步改为
lock:unlock
要放在try-finally的finally中确保一定会执行到
JDK1.5Condition
方法
- 图示:
- 已经将旧锁替换成新锁,那么锁上的监视器方法(wait,notify,notifyAll)也应该替换成新锁的监视器方法
- 而jdk1.5中将这些原有的监视器方法封装到了一个Condition对象中。
- 想要获取监视器方法,需要先获取Condition对象。
- Condition对象的出现其实就是替换了Object中的监视器方法。
- await();
- signal();
- signalAll();
- 将所有的监视器方法替换成了Condition。但是效率低的问题仍然存在。
/*
JDK1.5 中提供了多线程升级解决方案。
将同步Synchronized替换成现实Lock操作。
将Object中的wait,notify notifyAll,替换了Condition对象。
该对象可以Lock锁 进行获取。
该示例中,实现了本方只唤醒对方操作。
Lock:替代了同步代码块或者同步函数,将同步的隐式锁操作变成了显示锁操作。
同时更为灵活。可以一个锁上加上多组监视器。
lock() :获取锁
unlock() :释放锁,通常需要定义finally代码块中
Condition:替代了Object wait notify notifyAll
将这些监视器方法单独进行了封装,变成了Condition监视器对象。
可以任意锁进行组合。
await();
signal();
signalAll();
*/
import java.util.concurrent.locks.*;
class Resource
{
private String name;
private int count = 1;
private boolean flag = false;
// t1 t2
private Lock lock = new ReentrantLock();
private Condition condition_pro = lock.newCondition();
private Condition condition_con = lock.newCondition();
public void set(String name)throws InterruptedException
{
lock.lock();
try
{
while(flag)
condition_pro.await();//t1,t2
this.name = name+"--"+count++;
System.out.println(Thread.currentThread().getName()+"...生产者.."+this.name);
flag = true;
condition_con.signal();
}
finally
{
lock.unlock();//释放锁的动作一定要执行。
}
}
// t3 t4
public void out()throws InterruptedException
{
lock.lock();
try
{
while(!flag)
condition_con.await();
System.out.println(Thread.currentThread().getName()+"...消费者........."+this.name);
flag = false;
condition_pro.signal();
}
finally
{
lock.unlock();
}
}
}
class Producer implements Runnable
{
private Resource res;
Producer(Resource res)
{
this.res = res;
}
public void run()
{
while(true)
{
try
{
res.set("+商品+");
}
catch (InterruptedException e)
{
}
}
}
}
class Consumer implements Runnable
{
private Resource res;
Consumer(Resource res)
{
this.res = res;
}
public void run()
{
while(true)
{
try
{
res.out();
}
catch (InterruptedException e)
{
}
}
}
}
class ProducerConsumerDemo2
{
public static void main(String[] args)
{
Resource r = new Resource();
Producer pro = new Producer(r);
Consumer con = new Consumer(r);
Thread t1 = new Thread(pro);
Thread t2 = new Thread(pro);
Thread t3 = new Thread(con);
Thread t4 = new Thread(con);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
JDK1.5
解决办法-范例
API文档的“java.util.concurrent.locks 接口 Condition”下的示例,多生产多消费问题
:
下面这个范例代码最好自己能写一下
package com.Thread;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition(); //生产者
final Condition notEmpty = lock.newCondition();//消费者
final Object[] items = new Object[100]; //数组容器
int putptr,takeptr,count;
public void put(Object x) throws InterruptedException {
lock.lock(); //put拿到锁,take就拿不到了,lock之间是互斥的
try {
while (count == items.length)
//当生产够100 之后停止生产
notFull.await();
items[putptr] = x; // 生产一个,存一个
//如果存到100 将数组长度置为零
if (++putptr == items.length) putptr = 0;
++count; // 计生产数量
notEmpty.signal();//唤醒消费者线程,出餐了
}finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
//如果没有生产,则等待
while (count == 0)
notEmpty.await();
Object x = items[takeptr]; // 从0角标开始消费
// 消费至数组长度,则置为0。
if (++takeptr == items.length) takeptr = 0;
--count; //数量减少1
notFull.signal(); //唤醒生产者线程,点餐了
return x;
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
}
}
wait和sleep区别
wait和sleep区别: 分析这两个方法:从执行权和锁上来分析:
wait:可以指定时间也可以不指定时间。不指定时间,只能由对应的notify或者notifyAll
来唤醒。
sleep:必须指定时间,时间到自动从冻结状态转成运行状态(临时阻塞状态)。
wait:线程会释放执行权,而且线程会释放锁。
Sleep:线程会释放执行权,但不是不释放锁。
停止线程的方式
定义标记
线程的停止:
1. 通过stop方法就可以停止线程。但是这个方式过时了。
1. run方法结束
stop方法已经过时。
如何停止线程?
只有一种,run方法结束。
怎么结束run方法?开启多线程运行,运行代码通常是循环结构。
只要控制住循环,就可以让run方法结束,也就是线程结束。
一般run方法里肯定定义循环。所以只要结束循环即可。
第一种方式:定义循环的结束标记。
第二种方式:如果线程处于了冻结状态,是不可能读到标记的,这时就需要通过Thread类中的interrupt方法,将其冻结状态强制清除。让线程恢复具备执行资格的状态,让线程可以读到标记,并结束。
Thread类提供该方法 interrupt()
;
停止线程方式-Interrupt
守护线程-setDaemo
setDaemon(true)
:将该线程标记为守护线程或后台线程。将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,
Java 虚拟机退出。该方法必须在启动线程前调用。
听守护听不懂,那就换个词,理解为后台线程,我们以前做的哪些线程叫做前台线程
这个有什么区别也:
后台线程的特点在于它和前台线程都正常进行开启,就是结束不一样,运行都一样互相抢cpu
的执行权,结束不一样,前台线程你必须要进行一个手动结束,比如设定标记的形式来进行结束,你不结束它就会一直在哪儿等待,在哪儿耗费资源,对于后台进程,如果所有的前台线程都结束了,后台线程无论处于什么状态,都自动结束
class StopThread implements Runnable
{
private boolean flag =true;
public void run()
{
while(flag)
{
System.out.println(Thread.currentThread().getName()+"....run");
}
}
public void changeFlag()
{
flag = false;
}
}
class StopThreadDemo
{
public static void main(String[] args)
{
StopThread st = new StopThread();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
t1.setDaemon(true);
t2.setDaemon(true);
t1.start();
t2.start();
int num = 0;
while(true)
{
if(num++ == 60)
{
//st.changeFlag();
//t1.interrupt();
//t2.interrupt();
break;
}
System.out.println(Thread.currentThread().getName()+"......."+num);
}
System.out.println("over");
}
}
其他方法-join等
join:临时加入一个线程的时候可以使用join方法。
当A线程执行到了B线程的join方式。A线程处于冻结状态,释放了执行权,B开始执行。
A什么时候执行呢?只有当B线程运行结束后,A才从冻结状态恢复运行状态执行。
—————————< java.lang.Thread
>————
interrupt()
:中断线程。
setPriority(int newPriority)
:更改此线程的优先级。注意参数:Thread.MAX_PRIORITY
getPriority()
:返回此线程的优先级。
toString()
:返回该线程的字符串表示形式,包括线程名称、优先级和线程组。
如图:
Thread.yield():
暂停当前正在执行的线程对象,并执行其他线程。
/*
join:
当A线程执行到了B线程的.join()方法时,A就会等待。等B线程都执行完,A才会执行。
join可以用来临时加入线程执行。
*/
class Demo implements Runnable
{
public void run()
{
for(int x=0; x<70; x++)
{
System.out.println(Thread.currentThread().toString()+"....."+x);
Thread.yield();
}
}
}
class JoinDemo
{
public static void main(String[] args) throws Exception
{
Demo d = new Demo();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();
//t1.setPriority(Thread.MAX_PRIORITY);
t2.start();
//t1.join(); //t1 线程要申请加入进来,运行。临时加入一个线程运算时可以使用join方法。
for(int x=0; x<80; x++)
{
//System.out.println("main....."+x);
}
System.out.println("over");
}
}
多线程-面试题
class ThreadTest
{
public static void main(String[] args)
{
new Thread()
{
public void run()
{
for(int x=0; x<100; x++)
{
System.out.println(Thread.currentThread().getName()+"....."+x);
}
}
}.start();
for(int x=0; x<100; x++)
{
System.out.println(Thread.currentThread().getName()+"....."+x);
}
Runnable r = new Runnable()
{
public void run()
{
for(int x=0; x<100; x++)
{
System.out.println(Thread.currentThread().getName()+"....."+x);
}
}
};
new Thread(r).start();
//new Test1().start();
}
}
/*
class Test1 extends Thread
{
public void run()
{
for(int x=0; x<100; x++)
{
System.out.println(Thread.currentThread().getName()+"....."+x);
}
}
}
*/
举个线程中的小例子
/*class Test implementsRunnable
{
public void run(Thread t)
{}
}*/
//如果错误 错误发生在哪一行?错误在第一行,应该被abstract修饰
//这个run方法是子类特有的方法吧,
//一个类如果实现了一个接口 ,如果接口里面抽象方法没有被覆盖的话,该类是抽象的,要被abstract修饰才行
class ThreadTest
{
public static void main(String[] args)
{
new Thread(new Runnable()
{
public void run()
{
System.out.println("runnable run");
}
})
{
public void run()
{
System.out.println("subThread run");
}
}.start();
///本来吧,是要用任务的,你子类把父类的run方法覆盖了,就复写了啊,那就以子类为主了呗
}
}
class MyThread extends Thread{
public void run(){
try {
Thread.currentThread().sleep(3000);
} catch (InterruptedException e) {
}
System.out.println("MyThread running");
}
}
public class ThreadTest{
public static void main(String argv[]) {
MyThread t = new MyThread();
t.run();
t.start();
System.out.println("Thread Test");
}
}
/*
代码分析过程:
MyThread t = new MyThread();
创建了一个线程。
t.run();
调用MyThread对象的run方法。
这是只有一个线程在运行就是主线程。
当主线程执行到了run方法中的sleep(3000);时。
这是主线程处于冻结状态。程序并没有任何执行。
当3秒过后,主线程打印了 MyThread running。 run方法执行结束。
t.start();
开启了t线程。
有两种可能情况。
第一种,主线程在只执行了t.start()后,还具有执行权,继续往下执行,
打印了Thread Test。主线程结束。
t线程获取执行权,调用自己的run方法。然后执行的sleep(3000);冻结3秒。
3秒后,打印MyThread running t线程结束,整个程序结束。
第二种情况:
主线程执行到t.start();开启了t线程,t线程就直接获取到了执行权。
就调用自己的run方法。
指定到sleep(3000).t线程冻结3秒,这是t线程就是释放了执行权。
那么主线程开始执行打印了Thread Test,主线程结束。
等到3秒后,t线程打印MyThread running ,然后t线程结束。
程序结束。
*/
小节
进程:正在执行的程序。
线程:是进程中用于控制程序执行的控制单元(执行路径,执行情景)
进程中至少有一个线程。
对于JVM
,启动时,只好有两个线程:jvm
的主线程。jvm
的垃圾回收线程。
如何在程序中自定义线程呢?
Java给我们提供了对象线程这类事物的描述。该类是Thread
该类中定义了,
创建线程对象的方法(构造函数).
提供了要被线程执行的代码存储的位置(run())
还定义了开启线程运行的方法(start()).
同时还有一些其他的方法用于操作线程:
static Thread currentThead():
String getName():
static void sleep(time)throws InterruptedException:
要运行的代码都是后期定义的。
所以创建线程的第一种方式是:继承Thread类。原因:要覆盖run方法,定义线程要运行的代码。
如果自定义的类中有多线程要运行的代码。但是该类有自己的父类。
那么就不可以在继承Thread。怎么办呢?
创建线程的两种方式。★★★★★
-
--继承Thread
- 继承Thread类。
- 覆盖run方法。将线程要运行的代码定义其中。
- 创建Thread类的子类对象,其实就是在创建线程,调用start方法。
-
实现Runnable接口。
- 步骤:
- 定义了实现Runnable接口。
- 覆盖接口的run方法。将多线程要运行的代码存入其中。
- 创建Thread类的对象(创建线程),并将Runnable接口的子类对象作为参数传递给Thread的构造函数。
- 为什么要传递?因为线程要运行的代码都在Runnable子类的run方法中存储。所以要将该run方法所属的对象
传递给Thread。让Thread线程去使用该对象调用其run方法。 - 调用Thread对象的start方法。开启线程。
- 步骤:
动手写代码。
Thread和实现Runnable接口的区别:
Java给我们提供了一个规则。Runnable接口。
如果自定义类不继承Thread,也可以实现Runnable接口。并将多线程要运行的代码存放在Runnable的run方法中。
这样多线程也可以帮助该类运行。
这样的操作有一个好处:避免了单继承的局限性。实现方式,因为避免了单继承的局限性,所以创建线程建议使用第二种方式。
##############################
第一重点:创建线程的两种方式#
##############################
作为了解:
线程的状态。
- 被创建。
- 运行。
- 冻结。
- 临时阻塞
- 消亡
特殊的状态:临时阻塞
该临时状态的特点:
具备了执行资格,但不具备执行权。
冻结状态的特点:
放弃了执行资格。
多线程具备随机性。因为是由cpu
不断的快速切换造成的。
就有可能会产生多线程的安全问题。
问题的产生的原因:
几个关键点:
1,多线程代码中有操作共享数据。
2,多条语句操作该共享数据。
当具备两个关键点时,
有一个线程对多条操作共享数据的代码执行的一部分。还没有执行完,另一个线程开始参与执行。
就会发生数据错误。
解决方法:
当一个线程在执行多条操作共享数据代码时,其他线程即使获取了执行权,也不可以参与操作。
Java就对这种解决方式提供了专业的代码。
同步
同步的原理:就是将部分操作功能数据的代码进行加锁。
示例:火车上的卫生间。
同步的表现形式:
1,同步代码块。
2,同步函数。
两者有什么不同:
同步代码块使用的锁是任意对象。
同步函数使用的锁是this。
注意:对于static的同步函数,使用的锁不是this。是 类名.class 是该类的字节码文件对象。
涉及到了单例设计模式的懒汉式。
同步的好处:解决了线程的安全问题。
弊端:
较为消耗资源。
同步嵌套后,容易死锁。
要记住:同步使用的前提:
1,必须是两个或者两个以上的线程。
2,必须是多个线程使用同一个锁。
这是才可以称为这些线程被同步了。
死锁代码一定会写。但开发时一定注意避免。
线程间的通信。等待/唤醒机制。
|--概念:多个线程,不同任务,处理同一资源。
|--等待唤醒机制。使用了锁上的 wait notify notifyAll
. ★★★★★
|--生产者/消费者的问题。并多生产和多消费的问题。 while判断标记。用notifyAll唤醒对方。★★★★★
|--JDK1.5
以后出现了更好的方案,★★★
Lock接口替代了synchronized
Condition接口替代了Object中的监视方法,并将监视器方法封装成了Condition
和以前不同的是,以前一个锁上只能有一组监视器方法。现在,一个Lock锁上可以多组监视器方法对象。
可以实现一组负责生产者,一组负责消费者。
|--wait和sleep的区别。★★★★★
8,停止线程的方式。
|--原理:
|--表现:--中断。
9,线程常见的一些方法。
|--setDaemon()
|--join();
|--优先级
|--yield();
|--在开发时,可以使用匿名内部类来完成局部的路径开辟。
##############################
第二重点:同步的所有特性
##############################
class Thread
{
private Runnable target;
Thread()
{
}
Thread(Runnable target)
{
this.target = target;
}
public void run()
{
if(target!=null)
target.run();
}
}
class Test implements Runnable
{
public void run()
{}
}
main()
{
Test q = new Test();
Thread t = new Thread(q);
}
class Demo extends Thread
{
public void run()
{
}
}
class
{
public static void main(String[] args)
{
System.out.println("Hello World!");
}
}