一、多线程的基础概念 1、基本概念 (1)程序(program):为了完成一个功能/任务/需求,而选择一种编程语言(Java/C++/python等)编写的一组指令的集合。 无论你选择哪种编程语言,最终都要“编译/解释”等方式把代码转换为CPU能认识的“指令”。 程序没有运行时,它是静态的,一组数据存在硬盘上。
(2)进程(process):程序的一次运行,程序的运行状态,它是动态的。 ? ? ? ? 进程是操作系统分配资源(例如:内存等)的最小单位。 ? ? ? ? 如果是同一个软件/程序运行了两次,分别为两个进程,它们之间也是“独立”的。 ? ? ? ? 两个进程之间的通信(数据信息的交换)是比较麻烦的。可以通过访问同一个“文件”的方式交换数据,或者通过网络通信。 ? ? ? ? 两个进程之间的“切换”的成本也比较高。
? ? ? ? 咱们的CPU的运行是非常快的,CPU的资源是有限(数量有限)。 ? ? ? ? CPU可以在多个进程之间来回“切换”,同时为大家服务。 ? ? ? ? CPU会给每一个“进程”分配时间片(非常短的一个时间),当时间片到了,就会切换到另一个进程去运行。 ? ? ? ? CPU发生切换时,会给进程“拍一个照片(比喻)”,记录当前进程的一些状态,以及它执行到哪个指令了。 (3)线程(thread):是进程中的其中一条执行路径。 ? ? ? ? 刚才说,①进程的内存等资源是独立的,如果多个任务之间需要进行数据共享等操作,如果通过多个进程的方式来完成的话, ? ? ? ? ? ? ? 数据共享比较麻烦。 ? ? ? ? ? ? ? ②CPU在进程之间的切换的成本也比较高,记录的东西比较多。 ? ? ? ? 希望完成多个任务,但是不是通过多个进程的方式来完成,而是通过多个线程的方式来完成。 ? ? ? ? 同一个进程的多个线程是可以共享内存的,这个内存的是指堆、方法区等可以共享,像栈内存、程序计数器是不同共享的。 ? ? ? ? 同一个进程的多个线程之间的切换的成本比多个进程之间的切换成本要低。
? ? ? ? 线程又被称为轻量级的进程,它是CPU管理和切换的最小单位。
? ? ? ? 进程的粒度>线程。
? ? ? ? 每一个进程是至少有一个线程。可以有多个线程。 ? ? ? ? 之前写的Java程序,至少有一个main线程。其实后台还有别的线程:GC(垃圾回收线程)、异常检测和处理线程、类加载等。
2、回忆 JVM的内存 (1)方法区:存储加载的类信息,静态变量,常量池等 (2)堆:new出来的对象 (3)Java虚拟机栈:存方法体是用Java语言编写的方法的局部变量等信息 ? ? ? ? ? ? 方法运行时有“入栈”和“出栈”的过程。 (4)本地方法栈:方法体是用C语言编写的方法的局部变量等信息,native关键字修饰的方法。 (5)程序计数器:存储每一个线程“快照”的信息,比如CPU下一次运行它要执行的指令。
3、需要简单了解的另外两个概念(JavaSE阶段是了解) 并行:无论是宏观还是微观的角度都是同时进行。如果是一个CPU是无法实现并行的,需要多个CPU的来支持。 并发:从宏观的角度是同时进行的,但是从微观的角度是交替进行的。 ? ? 比喻:泡面、吃泡面和接电话 ? ? ? ? 并行:一边烧水、一边在准备面和调料 ? ? ? ? 并发:一边吃泡面,一边接电话 (从微观的角度来说,说话的时候是没法吃)
切换有随机性。
二、Java是如何支持编写多线程的程序的? 1、Java只是支持多线程,真正的多线程的实现不是由Java语言来完成的,Java只负责处理上层的一些事情,真正的底层实现还要交给C/操作系统来完成。 Java提供了java.lang.Thread类,java.lang.Runnable接口,java.util.concurrent包及其子包下的相关API来完成。 实现多线程一共有四种方式: (1)java.lang.Thread类(今天) (2)java.lang.Runnable接口(今天) (3)java.util.concurrent.Callable 接口(高级学习) (4)线程池(高级学习)
2、今天的两种方式 (1)继承Thread类 步骤: 第一步:编写一个类(可以有名字,可以匿名)继承Thread类 第二步:重写Thread类的一个方法:public void run() 你要这个线程完成的任务代码就写到run方法中 第三步:创建自定义线程类的对象,例如:MyThread 第四步:启动线程,调用线程对象的start()
(2)实现Runnable接口 步骤: 第一步:编写一个类(可以有名字,可以匿名)实现Runnable接口 第二步:重写Runnable接口的抽象方法:public void run() 你要这个线程完成的任务代码就写到run方法中 第三步:创建自定义线程类的对象,例如:MyRunnable 第四步:创建一个Thread类的对象(可以看成代理,也可以看成真正的线程对象) 要让Thread类对象帮我们“代理” MyRunnable的run方法。 在创建Thread类的对象时,通过构造器传参的方式,把 MyRunnable的对象传给它 第五步:启动线程,调用Thread类的对象的start()
public class TestThreadAPI {
public static void main(String[] args) {
MyThread my = new MyThread();
//原来是通过my对象直接调用run()方法
// my.run(); //如果是这种方式,并没有启动第二个线程与main同时运行
//启动线程
my.start(); //如果是这种方式,会启动另一个线程与main同时运行
//start()方法是从父类Thread类继承的
MyRunnable m2 = new MyRunnable();
// m2.start();//错误的,因为MyRunnable类没有start方法
Thread t = new Thread(m2); //调用了一个有参构造 public Thread(Runnable target)
//m2给Thread类中的target成员变量赋值
t.start(); //启动的是t线程,但是JVM调用t的run方法
/*
java.lang.Thread类的run方法的源代码
@Override
public void run() {
if (target != null) {
target.run();
}
}
*/
//在main线程中,打印1-10之间的奇数
for (int i = 1; i <=10 ; i+=2) {
System.out.println("main:" + i);
}
}
}
class MyThread extends Thread{
@Override
public void run() {
//例如:这里希望这个线程可以打印1-10之间的偶数
for (int i = 2; i <=10 ; i+=2) {
System.out.println("MyThread的线程对象:" + i);
}
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
//例如:这里希望这个线程可以打印10-20之间的所有数字
for (int i = 10; i <=20 ; i++) {
System.out.println("MyRunnable线程对象:" + i);
}
}
}
三、java.lang.Thread类的其他API 1、获取线程的一些基本信息, 例如:线程的名称,线程id,线程的优先级等。
线程名称,如果没有手动指定,除了main线程以外,其他线程默认是Thread-编号,编号从0开始。 线程名称也可以由程序员手动指定。
String getName(); ?获取线程名 int getId(); ? ? ? 获取线程id int getPriority(); 获取优先级 ? ? Thread类中有几个常量: ? ? ? ? MAX_PRIORITY:最高优先级 ?10 ? ? ? ? MIN_PRIORITY:最低优先级 ?1 ? ? ? ? NORM_PRIORITY:默认优先级,普通优先级 ?5 void setPriority(int newPriority):设置优先级
优先级高的线程,获得CPU调用的“概率”更高,但是不代表优先级低完全没有机会。
2、获取当前线程对象(静态方法) public static Thread currentThread()
3、Thread类的构造器 Thread() Thread(Runnable target) Thread(String name) Thread(Runnable target, String name)
public class TestThreadAPI2 {
public static void main(String[] args) {
MyDemo my1 = new MyDemo();//默认名称Thread-0
//修改线程的信息,最后是在它启动之前修改
my1.setPriority(10);
// my1.setPriority(100);//IllegalArgumentException:非常参数错误
my1.start();
System.out.println("my1.id = " + my1.getId());
System.out.println("my1.优先级:" + my1.getPriority());
MyDemo my2 = new MyDemo("线程2");
my2.start();
MyExample myExample = new MyExample();
Thread t = new Thread(myExample);//默认名称Thread-1
t.start();
Thread t2 = new Thread(myExample,"线程4");
t2.start();
}
}
class MyDemo extends Thread{
public MyDemo() {
}
public MyDemo(String name) {
super(name);
}
public void run(){
for(int i=1; i<=100; i++) {
System.out.println("MyDemo线程名称:" + getName());//getName()方法从Thread类继承的
}
}
}
class MyExample implements Runnable{
//重写接口的抽象方法的快捷键:Ctrl + o 或 Ctrl + i
@Override
public void run() {
for(int i=1; i<=100; i++) {
// System.out.println("线程名称:" + getName());//错误,MyExample没有这个方法,父类Object也没有这个方法
System.out.println("MyExample线程名称:" + Thread.currentThread().getName());//getName()方法是Thread类的
//Thread.currentThread()获取当前线程对象,哪个线程对象在执行这句代码,得到的就是哪个线程对象
}
}
}
4、线程控制的方法 (1)public static void sleep(long millis) throws InterruptedException ?线程休眠 ? ? millis:参数的单位是毫秒,1秒=1000毫秒 public static void sleep(long millis,int nanos)throws InterruptedException ? ? nanos:参数的单位是纳秒
(2)public final void join() throws InterruptedException ? ? ? ? 线程无限加塞 ? ?public final void join(long millis) ?throws InterruptedException ? 线程加塞millis毫秒 ? ?public final void join(long millis, int nanos) throws InterruptedException ?线程加塞millis毫秒+nanos纳秒
(3)public static void yield():暂停当前线程 当前线程如果正在被CPU调度执行的话,遇到yield()这句代码,当前线程就让出CPU。 它立刻加入和其他线程抢CPU的队伍。可能下次还是它抢到,也可能被别人抢走了。
public class TestThreadAPI3 {
public static void main(String[] args) {
//演示sleep的
/* for(int i=1; i<=5; i++){
System.out.println("hello");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}*/
// new Example1().start();
//演示join
Example1 e1 = new Example1();
e1.start();
for(int i=1; i<=5; i++){
if(i==2){
/* try {
// e1.join(); //e1线程把当前main线程给阻塞了,反过来说,main线程被e1线程给加塞了,必须等e1线程完事,main线程才能继续
// e1.join(2000);//2秒
} catch (InterruptedException e) {
e.printStackTrace();
}*/
Thread.yield();//main线程暂停
}
System.out.println("main:" + i);
}
}
@Test
public void test(){
//本身test()也是一个线程,主线程,但是它和我们上面的main线程有点不同
/*
main线程中启动了多个线程时,如果其他线程没有结束,main方法的代码就算运行完了,它也不会停止(因为main方法停止,就会导致JVM退出)。
JUnit的方式的话,测试方法结束了,就直接退出JVM,不等其他线程结束。
*/
new Example1().start();
}
}
class Example1 extends Thread{
public void run(){
for(int i=1; i<=100; i++){
/* try {
// Thread.sleep(10000);
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
System.out.println(i);
}
}
}
五、线程安全问题 卖票的例子:现在有一场电影票,票数是10张,分为3个窗口同时卖票。
public class TestSafe1 {
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);
t1.start();
t2.start();
t3.start();
}
}
class Ticket implements Runnable{
/*
实例变量,多个线程可以共享,但是需要多个线程使用的是同一个实例对象
*/
private int total = 10; //总票数
public void run(){
while(total>0){
total--;
System.out.println(Thread.currentThread().getName() + "窗口卖了1张票,剩余"+ total + "张");
}
}
}
1、卖票例子出现了问题: ? ? 现象:出现了重复票,或者负数票等问题。 ? ? 原因:三个线程同时访问“同一个资源(这里就是同一个total)”,并且大家都对它进行读(看)和写(修改), ? ? ? ? ? 使得其中一个线程在它的“一个业务单元”的操作过程中,还没有用完这个资源时,就被其他线程影响了。 ? ? 这个情况就叫做线程安全问题。
? ? 比喻:两个人合租,就一个卫生间,假设每个人都有5分钟使用时间,如果5分钟内能够完成自己的一次业务, ? ? ? ? 完全没问题,交替使用。但是有时候,5分钟不够解决业务,那么另一个人就进来了,很尴尬。
2、如何解决? 加锁
? ? 当一个线程在使用这个“共享”资源时,可以对这个“资源”进行加锁,在我没有使用完之前,其他线程等待。 ? ? 只有这个线程使用完了,释放锁,其他线程才有机会。
3、Java中如何加锁呢? JavaSE阶段讲同步锁,后期juc中有更丰富各种锁。 同步锁,依赖于一个关键字 synchronized
4、同步锁的使用形式有两种: (1)同步代码块 (2)同步方法
5、同步方法: 语法格式:【其他修饰符】 synchronized ?返回值类型 ?方法名(【形参列表】) 【throws 异常列表】{} 锁: ? ? 非静态方法的锁对象是this对象 ? ? 静态方法的锁对象是方法所在类的Class对象,例如:Piao类的静态方法,它的锁对象是Piao.class对象 ? ? (Class对象在反射章节会讲,目前只要知道它是代表一个类的对象,只要是同一个类,Class对象就是同一个)
6、同步锁的要求 (1)想要让锁起作用,必须保证这些“竞争”关系的线程使用同一个“锁对象“。 ? ? 比喻:卫生间加锁 ? ? ? ? 门上锁,大家一个锁,一个人锁,其他人进不去,共享同一个锁。 ? ? ? ? 门上的锁坏了,如果大家约定,门上贴了“有人”,其他人默认为锁上了,就不进去也安全。 ? ? ? ? ? ? ? ?但是如果大家没有约定,“汪飞”认为他贴了“有人”,其他人就不会进去,但是没有约定,其他人不承认这个锁,直接进去了,不安全。 ? ? Java:锁对象,在对象头中有一个标识,如果有“线程”占用这个锁了,它会记录这个线程的id, ? ? ? ? 如果这标记有线程占了,其他线程就等着,等到它清除这个标记。 (2)需要加锁的代码范围:一次业务逻辑 ?例如:卖票 ?判断票数和减票数应该一气呵成(中间不能让其人线程干扰)
public class TestSafe5 {
public static void main(String[] args) {
Piao piao = new Piao();
Saler s1 = new Saler("窗口一", piao);
Saler s2 = new Saler("窗口二", piao);
Saler s3 = new Saler("窗口三", piao);
s1.start();
s2.start();
s3.start();
}
}
/*class Saler extends Thread{
private Piao piao;
public Saler(String name, Piao piao) {
super(name);
this.piao = piao;
}
*//*
run方法是非静态的,它的锁对象是this,这里的this是Saler的对象
思考:上面的三个线程s1,s2,s3的this不是同一个
*//*
public synchronized void run(){
while(true){
if(piao.getTotal()>0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
piao.sale();
}else{
System.out.println("卖完了");
break;
}
}
}
}*/
class Saler extends Thread{
private Piao piao;
public Saler(String name, Piao piao) {
super(name);
this.piao = piao;
}
public void run(){
while(true){
if(piao.getTotal()>0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
piao.sale();
}else{
System.out.println("卖完了");
break;
}
}
}
}
//资源类
class Piao{
private int total = 10;
//卖票,调用一次这个方法,卖一张票
//锁对象是this
public synchronized void sale(){ //这里锁的是方法体的代码,一个线程在执行时,其他线程就不能进来,等它执行完,释放后,才能让其他线程调用
if(total>0) {
total--;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余" + total + "张");
}
}
public int getTotal() {
return total;
}
}
7、同步代码块 (1)语法结构 ? ? synchronized(锁对象){ ? ? ? ? 需要加锁的一次业务逻辑代码 ? ? }
例如:卖票的一次业务逻辑代码,包含判断票数和减票数
(2)锁对象怎么选 必须保证竞争关系的多个线程,使用同一个锁对象,类型不重要,重要的是同一个锁对象
(3)锁的代码范围:一次业务逻辑代码
public class TestSafe6 {
public static void main(String[] args) {
Piao piao = new Piao();
Saler s1 = new Saler("窗口一", piao);
Saler s2 = new Saler("窗口二", piao);
Saler s3 = new Saler("窗口三", piao);
s1.start();
s2.start();
s3.start();
}
}
class Saler extends Thread{
private Piao piao;
// private static Object lock = new Object();
public Saler(String name, Piao piao) {
super(name);
this.piao = piao;
}
public void run(){
while(true){
// synchronized (this) {//this这里不合适,这里 this是Saler的对象,有3个,不是同一个
// synchronized (lock) {//可以的
synchronized (piao) {//也可以
if (piao.getTotal() > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
piao.sale();
} else {
System.out.println("卖完了");
break;
}
}
}
}
}
//资源类
class Piao{
private int total = 10;
//卖票,调用一次这个方法,卖一张票
public void sale(){
total--;
System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余" + total + "张");
}
public int getTotal() {
return total;
}
}
六、单例设计模式 1、Java(包括其他语言)的常用/经典设计模式一共23种。 设计模式:之前的程序员在开发过程中,针对一些问题,设计的解决方案,一套经验的总结。 ? ? ? ? 有点类似于解决xx问题的代码模板,套路。
? 例如: ? ? 问题的场景,保证某个类的对象在整个程序运行期间只有一个。 ? ? 为了解决这样的问题,有人提出了一个解决方案,单例设计模式。 ? ? 单例:唯一的实例。
2、单例设计模式的实现(掌握) 第一种:饿(恶)汉式单例设计模式 (1)枚举形式 (2)相当于JDK1.5之前实现枚举的效果 (3)对(2)的改版,把唯一对象私有化,通过静态方法提供给使用者 java.lang.Runtime
饿汉式单例设计模式:无论你现在是否需要这个对象,我在初始化这个类的时候,都直接把这个类的对象创建好。
第二种:懒汉式单例设计模式 延迟创建对象,等你什么时候需要,什么时候再创建,而不是类初始化时直接创建。 (4)需要考虑线程安全问题 (5)内部类的形式
public class TestSingle {
@Test
public void test01(){
SingleOne one = SingleOne.INSTANCE;
SingleOne two = SingleOne.INSTANCE;
System.out.println(one == two);//比较的是地址
}
@Test
public void test02(){
SingleTwo one = SingleTwo.INSTANCE;
SingleTwo two = SingleTwo.INSTANCE;
System.out.println(one == two);//比较的是地址
}
@Test
public void test03(){
SingleThree one = SingleThree.getInstance();
SingleThree two = SingleThree.getInstance();
System.out.println(one == two);//比较的是地址
}
@Test
public void test04(){
Single.method();
Single.method();
Single.method();
//并没有需要使用Single类的对象,但是Single类要初始化,初始化过程中,就创建了Single的对象
Single instance = Single.getInstance();//在这里才获取对象,相当于对象的生命周期远远大于我需要它的时间
}
@Test
public void test05(){
SingleFour s1 = SingleFour.getInstance();
SingleFour s2 = SingleFour.getInstance();
System.out.println(s1 == s2);//false or true
}
SingleFour s1;
SingleFour s2;
@Test
public void test06(){
Thread t1 = new Thread(){
public void run(){
s1 = SingleFour.getInstance(); //这个线程获取SingleFour的对象给s1变量赋值
}
};
t1.start();
Thread t2 = new Thread(){
public void run(){
s2 = SingleFour.getInstance(); //这个线程获取SingleFour的对象给s1变量赋值
}
};
t2.start();
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
}
@Test
public void test07(){
SingleFive.method();
SingleFive five = SingleFive.getInstance();
}
}
enum SingleOne{
INSTANCE
}
class SingleTwo{
public static final SingleTwo INSTANCE = new SingleTwo();
private SingleTwo(){}
}
class SingleThree{
private static SingleThree INSTANCE = new SingleThree();
// private static final SingleThree INSTANCE = new SingleThree();//是否final不重要
private SingleThree(){}
public static SingleThree getInstance(){
return INSTANCE;
}
}
class Single{
private static Single instance = new Single();
private Single(){
}
public static Single getInstance(){
return instance;
}
public static void method(){
System.out.println("静态方法");
}
}
class SingleFour{
private static SingleFour instance;
private SingleFour(){
}
/* public static SingleFour getInstance(){
// return new SingleFour();//不是
}*/
//锁对象:静态方法的锁对象是方法所在类的Class对象 ,这里的话是SingleFour.class
//这个写法可以保证安全,但是不够完美
/* public synchronized static SingleFour getInstance(){
if (instance == null) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new SingleFour();
}
return instance;
}*/
public static SingleFour getInstance(){
/*
说这个写法好的原因:
只有最早判断instance==null的线程,才需要抢/竞争锁,去尝试创建唯一的实例对象,
之后,其他线程在用这个方法时,instance已经创建好了,就不需要去竞争锁了,直接返回instance
*/
if(instance == null) {
synchronized (SingleFour.class) {
if (instance == null) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new SingleFour();
}
}
}
return instance;
}
}
class SingleFive{
private SingleFive(){
}
//静态内部类
//虽然Inner是外部类的一个静态成员,但是它的初始化,也必须是有人用了这个类才会初始化
private static class Inner{
static SingleFive instance = new SingleFive(); //在Inner类初始化时创建SingleFive的对象
// 类初始化的过程是一定线程安全的
static{
System.out.println("内部类静态代码块");
}
}
public static SingleFive getInstance(){
return Inner.instance; //调用这个方法时,会用到Inner类,才会初始化Inner类
}
public static void method(){
System.out.println("外部类的静态方法");
}
}
|