java多线程、创建线程、线程安全

it2022-05-05  72

文章目录

第一章 多线程初步1.1 并发与并行1.2 线程与进程1.3 线程调度1.4 创建线程类1. 什么是主 (单) 线程2. 创建线程方式一(方式二在第二章的2.3) 第二章:线程2.1 多线程原理2.2 Thread类1. 获取线程名称的方法2. 设置线程的名称 (了解)3. Thread类的sleep方法 2.3 创建线程方式二:实现Runnable接口2.4 Thread和Runnable的区别2.5 匿名内部类方式实现线程的创建 第三章:线程安全问题3.1 线程安全3.2 线程同步和同步代码块3.3 同步方法3.4 静态的同步方法(了解)3.5 Lock方法

第一章 多线程初步

1.1 并发与并行

并发:指两个或多个事件在同一时间段内发生并行:指两个或多个事件在同一时刻发生

1.2 线程与进程

进程:程序是由一堆二进制数字组成的,是一个静态的文件;当程序运行起来之后,就变成了一个进程,是一个动态的概念。即进程是程序的一次执行过程。

比如下面的eclipse没有启动时,就是一个躺在C盘里面的程序而已,当我一启动它,它就会被复制到内存中运行,此时它是一个进程,可以在任务管理器中的进程表查看

线程:线程是进程中的一个执行单元,一个进程中至少有一个线程。一个进程中是可以有多个线程的,称之为多线程程序。

比如当我打开360安全卫士

1.3 线程调度

分时调度

所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。

抢占式调度

抢占式让优先级高的线程先使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),java使用的为抢占式调度

设置线程的优先级

线程优先级

哪一个线程的优先级高,它所获得的CPU时间的长度和概率都会大一些,比如下面的三个线程,我把它设置为下载JDK文档的线程优先级最高,那么会优先下载JDK文档。

1.4 创建线程类

1. 什么是主 (单) 线程

一般来说我们的程序是从上往下一直运行的,单线程一旦运行过程中间出现了什么意外,那整个程序都会终止,程序最终只能运行出部分的内容。

我们现在创建一个person类

public class person { //person的名字 private String name; //带参数的构造 public person(String name) { this.name = name; } //让person做一些打印的事情 public void run() { for(int i = 0; i < 5; i++) { System.out.println(name + i); } } }

创建一个调用person类的main方法

public static void main(String[] args) { person p1 = new person("小强"); p1.run(); //打印 System.out.println(0/0);//0不能做分母 person p2 = new person("旺财"); p2.run();//打印 } /*输出结果: 小强0 小强1 小强2 小强3 小强4 Exception in thread "main" java.lang.ArithmeticException: / by zero at Multithreading.Demo01MainThread.main(Demo01MainThread.java:9) */

我们用 System.out.println(0/0); 让程序异常,JVM会中断程序,那么小强的那部分就能打印出来,而旺财的那部分就无法打印出来,这就是单线程的弊端。如果是多线程,我们让线程1负责打印小强的那一部分,线程2负责打印旺财的那一部分,这样就算代码中间出现什么异常,都可以运行完主要的部分。注意打印输出中的Exception in thread “main”,这就是主线程或者说main线程,整个进程中就它一个线程而已。

2. 创建线程方式一(方式二在第二章的2.3)

创建新执行线程有两种方法。

**法一:**将类声明为 Thread 的子类。该子类重写 Thread 类的 run 方法。接下来可以分配并启动该子类的实例。例如,创建一个线程,它的任务是打印一些字符串:

//1.将子类Mythread声明为 Thread 的子类 public class Mythread extends Thread{ //2.重写Thread类的中run方法,设置线程任务 public void run() { for(int i = 0; i < 10; i++) { System.out.println("run方法"); } } }

下列代码会创建并启动一个线程:

public class Demo02practiceMythread { public static void main(String[] args) { //3.创建线程 Mythread mt = new Mythread(); //4.启动线程 mt.start(); for(int i = 0; i < 10; i++) { System.out.println("main方法"); } } } /*输出结果: main方法 run方法 run方法 run方法 run方法 run方法 run方法 run方法 run方法 run方法 run方法 main方法 main方法 main方法 main方法 main方法 main方法 main方法 main方法 main方法 */

**程序分析:**其实这里面有两个线程,main线程和Thread-0线程,两个线程并发地执行打印的任务;java属于抢占式调度,哪个线程的优先级高,哪一个线程优先执行;同一个优先级,随机选择一个执行。这里应该属于随机选择。

第二章:线程

2.1 多线程原理

2.2 Thread类

1. 获取线程名称的方法

/* * 获取线程的名称: * 1.使用Thread类中的方法getName() * String getName() 返回该线程的名称。 * 2. 可以先获取到当前正在执行的线程,使用线程中的方法getName()获取现成的名称。 * static Thread currentThread() 返回对当前正在执行的线程对象的引用 */

方式一:利用Thread类的gerName()

//将子类Mythread声明为 Thread 的子类 public class Mythread extends Thread{ //重写Thread类中的run方法 public void run() { //获取线程名称 String name = getName(); //打印名称 System.out.println(name); } } public class Demo02practiceMythread { public static void main(String[] args) { //创建一个线程mt Mythread mt = new Mythread(); //启动线程 mt.start(); //Thread-0 //直接新建两个线程并启动它 new Mythread().start(); //Thread-1 new Mythread().start(); //Thread-2 } } /*输出结果 Thread-0 Thread-1 Thread-2 */

方式二:利用静态方法Thread.currentThread()

//将子类Mythread声明为 Thread 的子类 public class Mythread extends Thread{ //重写Thread类中的run方法 public void run() { //获取线程名称 Thread t = Thread.currentThread(); System.out.println(t); } } public class Demo02practiceMythread { public static void main(String[] args) { //创建一个线程mt Mythread mt = new Mythread(); mt.start(); //Thread[Thread-0,5,main] //直接新建两个线程并start new Mythread().start(); //Thread[Thread-1,5,main] new Mythread().start(); //Thread[Thread-2,5,main] //获取main线程的线程名称,由于main没有继承Thread类,不能用getName方法 System.out.println(Thread.currentThread()); //Thread[main,5,main] } } /*输出结果: Thread[main,5,main] Thread[Thread-0,5,main] Thread[Thread-1,5,main] Thread[Thread-2,5,main] */

2. 设置线程的名称 (了解)

**方法一:**使用Thread类中的方法setName(名字)

void setName(String name)改变线程名称,称之与参数 name 相同。

示例:

写一个线程类

public class Mythread extends Thread{ //重写Thread类中的run方法 public void run() { //获取线程名称 System.out.println(Thread.currentThread().getName()); } }

在main里面改变它的名字

public class Demo02practiceMythread { public static void main(String[] args) { //创建一个线程mt Mythread mt = new Mythread(); //一般线程名称默认是Thread-0,这里把它改成小强 mt.setName("小强"); //启动线程 mt.start(); } } //输出结果是小强

**方法二:**创建一个带参数的构造方法,参数传递线程的名称;调用父类的带参构造方法,把线程名称传递给父类,让父亲(Thread)给子线程起一个名字,Thread(String name)分配的Thread对象

示例:

写一个线程类

public class Mythread extends Thread{ //无参构造函数 public Mythread() {} //带参构造函数 public Mythread(String name) { //把名字传给父类,让它来帮忙起名字 super(name); } public void run() { //获取线程名称 System.out.println(Thread.currentThread().getName()); } }

在main里面启动线程

public class Demo02practiceMythread { public static void main(String[] args) { //线程名称设置为旺财,并启动它 new Mythread("旺财").start(); } } //输出结果是旺财

3. Thread类的sleep方法

public static void sleep(long miles):是一个静态的方法,使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)

**示例:**每隔一秒钟打印数字 i

public static void main(String[] args) { for(int i = 0; i <= 5; i++) { System.out.println(i); //每隔1000毫秒打印一次 try { Thread.sleep(1000); //编译期异常,必须选择throws或try...catch } catch (InterruptedException e) { e.printStackTrace(); } } } }

2.3 创建线程方式二:实现Runnable接口

创建多线程程序的第二种方式:实现Runnable接口

java.lang.Runnable

​ Runnable接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为run的无参数方法。

java.lang.Thread类的构造方法

​ Thread (Runnable target) 分配新的 Thread 对象

​ Thread (Runnable target, String name)分配新的 Thread 对象

实验步骤:

1. 创建一个Runnable接口的实现类 在实现类中重写Runnable接口的run方法,设置线程任务创建一个Runnable接口的实现类对象创建一个Thread类对象,构造方法中传递Runnable接口的实现类对象调用Thread类中的start方法,开启新的线程执行run方法

代码演示

创建一个Runnable接口的实现类

//1. 创建一个Runnable接口的实现类 public class RunnableImpl implements Runnable{ //2. 在实现类中重写Runable接口的run方法,设置线程任务 public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "--->" + i); } } }

在main()中开启线程

public static void main(String[] args) { //3. 创建一个Runnable接口的实现类对象 RunnableImpl run = new RunnableImpl(); //4. 创建一个Thread类对象,构造方法中传递Runnable接口的实现类对象 Thread t = new Thread(run); //5. 调用Thread类中的start方法,开启新的线程执行run方法 t.start(); //main主线程也在打印 for(int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "---->" + i); } } /*输出结果是随机的,main和Thread-0两个线程的优先级一样,随机分配CPU时间 main---->0 Thread-0--->0 main---->1 Thread-0--->1 main---->2 Thread-0--->2 main---->3 Thread-0--->3 main---->4 Thread-0--->4 Thread-0--->5 Thread-0--->6 Thread-0--->7 main---->5 Thread-0--->8 main---->6 Thread-0--->9 main---->7 main---->8 main---->9 */

2.4 Thread和Runnable的区别

实现Runnable接口创建多线程程序的好处

避免了单继承的局限性

一个类只能继承一个类(一个人只能有一个亲爹),类继承了Thread类就不能继承其他类

可如果实现了Runnable接口,还可以继承其它的类,实现其它的接口

增强了程序的扩展性,降低了程序的耦合性

实现Runnable接口的方式,把设置线程任务和开启新线程进行了分离(解耦)

实现类中,重写了Run方法:用来设置线程任务

创建Thread类对象,调用start方法:用来开启新线程

2.5 匿名内部类方式实现线程的创建

匿名内部类方式实现线程的创建

匿名:没有名字

内部类:写在其他类内部的类

匿名内部类作用:简化代码

​ 把子类继承父类,重写父类方法,创建子类对象合一步完成

​ 把实现类接口,重写接口中的方法,创建子类对象等合一步完成匿名内部类的最终产物:子类/实现类对象,而这个类没有名字

格式:

​ new 父类/.接口(){

​ 重写父类/接口中的方法

};

代码示例

public class Anonymity { public static void main(String[] args) { //第一种:new Thread() new Thread() { //重写父类的run方法,设置线程任务 public void run() { for(int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + "---->" + "码农"); } } }.start(); //调用Thread的start方法 //第二种:Runnable r = new Runnable(); Runnable r = new Runnable() { //重写父类的run方法,设置线程任务 public void run() { for(int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + "---->" + "程序员"); } } }; //创建一个Thread类对象,构造方法中传递Runnable接口的实现类对象 new Thread(r).start(); //第三种:在2的基础3简化代码 new Thread(new Runnable() { //重写父类的run方法,设置线程任务 public void run() { for(int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + "---->" + "程序媛"); } } }).start(); } } /*结果 Thread-0---->码农 Thread-0---->码农 Thread-0---->码农 Thread-0---->码农 Thread-0---->码农 Thread-1---->程序员 Thread-1---->程序员 Thread-1---->程序员 Thread-1---->程序员 Thread-1---->程序员 Thread-2---->程序媛 Thread-2---->程序媛 Thread-2---->程序媛 Thread-2---->程序媛 Thread-2---->程序媛 */

第三章:线程安全问题

3.1 线程安全

​ 当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生。

​ 我们通过一个案例来演示线程安全问题:

​ 电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是“扫毒2”,本次电影的座位共50个,那就只能卖50张票出去。

​ 我们来模拟电影院的售票窗口,假如我们设置三个窗口同时卖票,一个线程负责一个窗口的运行。用Runnable接口子类来实现。

先写一个Runnable的实现类SaleTacket,重写run方法,设置线程任务(卖票)

public class SaleTicket implements Runnable{ //定义一个可以让多个线程共享的票源 private int tickets = 50; //设置线程任务:卖票 public void run() { //使用死循环,让卖票操作重复下去 while(true) { //先判断票是否存在 if(tickets > 0) { //为了提高问题出现的概率,更好地说明题目,我们让每次循环过后都小睡一下 try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } //tickets > 0,票存在,卖票 System.out.println(Thread.currentThread().getName() + "--->正在卖第" + tickets + "张票"); //卖完之后,tickets--; tickets--; } //票数小于0,退出死循环 else break; } } }

3个窗口,同时开始卖票

/* * 模拟卖票案例 * 创建3个线程,同时开启,对共享的票进行出售 */ public class MultiWindowsForSale { public static void main(String[] args) { //创建Runnable接口的实现类对象 SaleTicket run = new SaleTicket(); //创建Thread类,构造方法中传递Runnable接口的实现类对象 Thread t0 = new Thread(run); Thread t1 = new Thread(run); Thread t2 = new Thread(run); //调用start方法开启多线程 t0.start(); t1.start(); t2.start(); } }

实验结果

Thread-2--->正在卖第50张票 Thread-1--->正在卖第50张票 Thread-0--->正在卖第50张票 Thread-2--->正在卖第47张票 Thread-1--->正在卖第46张票 Thread-0--->正在卖第45张票 Thread-2--->正在卖第44张票 Thread-1--->正在卖第43张票 Thread-0--->正在卖第42张票 Thread-2--->正在卖第41张票 Thread-1--->正在卖第40张票 Thread-0--->正在卖第39张票 Thread-2--->正在卖第38张票 Thread-1--->正在卖第37张票 Thread-0--->正在卖第36张票 Thread-2--->正在卖第35张票 Thread-1--->正在卖第34张票 Thread-0--->正在卖第33张票 Thread-2--->正在卖第32张票 Thread-1--->正在卖第31张票 Thread-0--->正在卖第30张票 Thread-2--->正在卖第29张票 Thread-1--->正在卖第28张票 Thread-0--->正在卖第27张票 Thread-2--->正在卖第26张票 Thread-1--->正在卖第25张票 Thread-0--->正在卖第24张票 Thread-2--->正在卖第23张票 Thread-1--->正在卖第22张票 Thread-0--->正在卖第21张票 Thread-2--->正在卖第20张票 Thread-1--->正在卖第19张票 Thread-0--->正在卖第18张票 Thread-2--->正在卖第17张票 Thread-1--->正在卖第16张票 Thread-0--->正在卖第15张票 Thread-2--->正在卖第14张票 Thread-1--->正在卖第13张票 Thread-0--->正在卖第12张票 Thread-2--->正在卖第11张票 Thread-1--->正在卖第10张票 Thread-0--->正在卖第9张票 Thread-2--->正在卖第8张票 Thread-1--->正在卖第7张票 Thread-0--->正在卖第6张票 Thread-2--->正在卖第5张票 Thread-1--->正在卖第4张票 Thread-0--->正在卖第3张票 Thread-2--->正在卖第2张票 Thread-1--->正在卖第1张票 Thread-0--->正在卖第0张票 Thread-2--->正在卖第-1张票

程序分析

​ 我们做了一个实现类SaleTicket,并用它来new出一个对象run,但是我们却用这么一个对象制造出三个线程t0,t1和t2,而且让这三个线程同时运行,这就意味着这三个线程会共享到 run 对象中的数据 tickets。

​ 当数据 tickets 被共用的时候,就有可能出现下列问题:

​ 卖重复的票 (如结果中显示当卖第50张票时)。因为刚开始什么票都没卖时,三个线程一进来,判断 if (tickets > 0) 通过,就会都打印 “正在卖第50张票”;

​ 卖重复的票 (如结果中显示当卖第 -1 张票时)。因为还剩1张票时,三个进程判断 if(tickets > 0)都通过,这时候恰巧1号线程速度稍快,1号线程先卖,便打印出Thread-1--->正在卖第1张票,随后0号线程跟上,打印出Thread-0--->正在卖第0张票,最后2号线程跟上,打印出Thread-2--->正在卖第-1张票。

3.2 线程同步和同步代码块

上面的卖票案例中出现了线程安全的问题

卖了不存在的票和重复的票

解决线程安全问题的第一种方案:使用同步代码块

格式:

synchronized(锁对象){ 可能会出现线程安全问题的代码 (访问了共享数据的代码) }

注意:

代码块中的锁对象,可以是任意的对象

但是必须保证多个线程使用的锁对象是同一个

锁对象的作用:

把同步代码块锁住,只让一个线程在同步代码块中执行

把3.1中的Runnable的实现类SaleTacket改动一下就可避免线程安全问题

public class SaleTicket implements Runnable{ //定义一个多个线程共享的票源 private int tickets = 50; //创建一个锁对象,随便什么类的对象,String类的也行 Object obj = new Object(); //设置线程任务:卖票 public void run() { //使用死循环,让卖票操作重复下去 while(true) { //同步代码块,锁对象 synchronized(obj) { //先判断票是否存在 if(tickets > 0) { //票存在,卖票 System.out.println(Thread.currentThread().getName() + "--->正在卖第" + tickets + "张票"); //卖完之后,tickets--; tickets--; } else break; } } } }

结果分析(同步锁的原理)

使用了一个锁对象,这个锁对象叫同步锁,也叫对象锁,也叫对象监视器

3个线程一起抢夺CPU的执行权,谁抢到了谁执行run方法进行卖票

t0抢到了CPU的执行权,执行run方法,遇到synchronized代码块,这时t0会检查synchronized代码块是否有锁对象,发现有,就会获取到锁对象,进入到同步中执行t1抢到了CPU的执行权,执行run方法,遇到synchronized代码块,这时t1会检查synchronized代码块是否有锁对象,发现没有,t1进入到阻塞的状态,会一直等待t0线程归还锁对象,一直到t0线程执行完同步中的代码,会把锁对象归还给同步代码块,t1才能获取到锁对象进入到同步中执行总结:同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步,锁就相当于一个通行证,谁拿到锁了,谁就执行任务。同步保证了只能有一个线程在同步中执行共享数据,保证了安全,可程序频繁的判断锁,获取锁,释放锁,程序的效率会减低,不过为了安全,也是值得的。

3.3 同步方法

解决线程安全问题的二种方案:使用同步方法

同步方法:使用synchronized修饰的方法,就叫做同步方法,保证一个线程在执行该方法的时候,其它线程只能在方法外等着。

格式:

public synchronized void method(){ 可能会产生线程安全问题的代码 }

代码演示 (还是用3.1中的卖票的例子):

public class SaleTicket implements Runnable{ //定义一个多个线程共享的票源 private int tickets = 50; //设置线程任务:卖票 public void run() { //先判断票是否存在 while(true) { //调用synchronized过的方法 if(paytickets() == 0) break; } } //创建一个synchronized的方法,并把可能产生线程安全的问题的代码放进去 public synchronized int paytickets() { if(tickets > 0) { //票存在,卖票 System.out.println(Thread.currentThread().getName() + "--->正在卖第" + tickets + "张票"); //卖完之后,tickets--; tickets--; return 1; } return 0; } }

**代码分析:**其实这个也是通过对象锁来实现的,在3.2中,我们创立了一个Object的对象obj来作为对象锁,在这里我们也有一个对象锁,那就是this,即SaleTicket run = new SaleTicket();中的run对象。相当于下列代码

public void run() { //先判断票是否存在 while(true) { //调用synchronized方法 synchronized(this) { if(tickets > 0) { //票存在,卖票 System.out.println(Thread.currentThread().getName() + "--->正在卖第" + tickets + "张票"); //卖完之后,tickets--; tickets--; } else break; } } }

3.4 静态的同步方法(了解)

其实就是把3.3中的paytickets()方法改成静态的,即public static synchronized int paytickets()

如果是静态的方法,那被调用的成员数据tickets也要成静态的,即private static tickets = 0。那么问题来了,静态同步方法的锁对象还是this吗?

**不是!**this是创建对象之后产生的,静态方法优先于对象,静态的方法不属于任何一个对象,它属于这个类,所以静态方法的锁对象是本类的class属性---->class文件对象(反射)。

只需把SaleTicket类改成:

public class SaleTicket implements Runnable{ //定义一个多个线程共享的票源 private static int tickets = 50; //设置线程任务:卖票 public void run() { //先判断票是否存在 while(true) { //调用synchronized方法 if(paytickets() == 0) break; } } //创建一个synchronized的方法,并把可能产生线程安全的问题放进去 public static int paytickets() { synchronized(SaleTicket.class) { if(tickets > 0) { //票存在,卖票 System.out.println(Thread.currentThread().getName() + "--->正在卖第" + tickets + "张票"); //卖完之后,tickets--; tickets--; return 1; } return 0; } } }

3.5 Lock方法

解决线程安全问题的三种方案:使用Lock锁

java.util.concurrent.locks.lock接口

Lock实现提供了比用synchronized方法和语句可获得的更广泛的操作。

Lock接口中方法

void lock() 获取锁 void unlock() 释放锁

使用步骤

java.util.concurrent.locks.ReentrantLock implements Lock接口

在成员位置创建一个ReentrantLock对象。(ReentrantLock可重入的可重载的锁)在可能会出现安全问题的代码前调用Lock接口中的方法lock()获取锁在可能会出现安全问题的代码后调用Lock接口中的方法unlock()释放锁

代码演示(还是3.1中卖票的例子)

import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class SaleTicket implements Runnable{ //定义一个多个线程共享的票源 private int tickets = 50; //1.在成员位置创建一个`ReentrantLock`对象 Lock l = new ReentrantLock(); //设置线程任务:卖票 public void run() { //使用死循环,让卖票操作重复下去 while(true) { //2.在可能会出现安全问题的代码前调用Lock接口中的方法lock()获取锁 l.lock(); //先判断票是否存在 if(tickets > 0) { //票存在,卖票 System.out.println(Thread.currentThread().getName() + "--->正在卖第" + tickets + "张票"); //卖完之后,tickets--; tickets--; } else break; //3.在可能会出现安全问题的代码后调用Lock接口中的方法unlock()释放锁 l.unlock(); } } }

还有一个更好的写法,官方文档里面也是这么给的,就是unlock写在finally里面

写在finally里面的好处就是,无论有没有异常发生,都会把锁释放掉

import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class SaleTicket implements Runnable{ //定义一个多个线程共享的票源 private int tickets = 50; //1.在成员位置创建一个`ReentrantLock`对象。(`ReentrantLock`可重入的可重载的锁) Lock l = new ReentrantLock(); //设置线程任务:卖票 public void run() { //使用死循环,让卖票操作重复下去 while(true) { //2.在可能会出现安全问题的代码前调用Lock接口中的方法lock()获取锁 l.lock(); //先判断票是否存在 if(tickets > 0) { try{ //票存在,卖票 System.out.println(Thread.currentThread().getName() + "--->正在卖第" + tickets + "张票"); //卖完之后,tickets--; tickets--; }catch(Exception e) { e.printStackTrace(); }finally { //3.在可能会出现安全问题的代码后调用Lock接口中的方法unlock()释放锁 l.unlock(); } } else break; } } }

最新回复(0)