多线程
应用程序的执行都是cpu在做着快速的切换完成的。这个切换是随机的
1、进程
直译:正在进行中的程序
一个程序就是一个进程,而一个程序中的多个任务则被称为线程,进程其实就是一个静态的概念
2、线程(控制单元/执行路径)
- 就是进程中一个负责程序执行的控制单元(执行路径)
- 一个线程中可以执行多个路径,称之为多线程
- 开启多线程是为了同时运行多部分代码,每一个线程都有自己运行的内容。这个内容可以称为线程要执行的任务
3、多线程存在的利弊
- 多线程的好处:解决了多部分同时运行的问题
- 多线程的弊端:线程太多会使运行效率的降低
4、JVM中的多线程解析
JVM虚拟机的启动时本身就是多线程,至少有两个可以分析出来
-
执行main函数的线程
该线程的任务代码都定义在main函数中
-
负责垃圾回收的线程
该线程的任务代码都在垃圾回收器中
垃圾回收器实际上就是垃圾回收程序,可以通过系统System类中中的gc()方法唤醒调用
class Demo extends Object{
public void finalize(){
System.out.println("demo ok");
}
}
class ThreadDemo{
public static void main(){
new Demo();
new Demo();
System.gc();
new Demo();
System.out.println("hello zimo!");
}
}
// >: hello zimo!
// >: demo ok
// >: demo ok
5、创建线程
创建线程的目的是为了开启一条执行路径,去运行指定的代码和其他代码实现同时运行,运行的指定代码就是这个执行路径的任务。
所以开启线程是为了运行指定代码,只有继承Thread类,并复写run方法,将运行的代码定义在run方法中即可。
jvm创建的主线程的任务都定义在了主函数中,而自定义的线程任务运行在哪?
Thread类用于描述线程,线程是需要任务的,所以Thread类也有对任务的描述。这个任务就是通过Thread类中的run方法来体现。也就是说,run方法就是封装自定义线程运行任务的函数。
run方法就是定义在线程要运行的任务代码。
1、创建线程方式一:继承Thread类
- 定义一个类继承Thread类
- 覆盖Thread类中的run方法
- 直接创建Thread的子类对象创建线程
- 调用start方法开启线程并调用线程的任务run方法执行
多线程实现两个对象同时运行实例:
class Demo extends Thread{
private String name;
Demo(String name){
// super(name); // 给线程起个名
this.name = name;
}
public run(){
show();
}
public void show(){
for(int i = 0; i < 20; i++){
System.out.println(name + "....." + i + getName());
}
}
}
class ThreadDemo{
public static void main(){
Demo d1 = new Demo("zimo");
Demo d2 = new Demo("mozi");
// d1.run();
// d2.run();
d1.start(); // 开启线程,调用run方法
d2.start();
System.out.println("hello zimo!");
}
}
可以通过Thread的getName()获取线程的名称 Thread - 编号(从0开始)
-
获取当前运行线程名称 Thread.currentThread().getName() 获取线程名称
主线程的名称:main
2、创建线程方式二:实现Runnable接口
定义类实现Runnable接口
覆盖接口中的run()方法,将线程的任务代码封装到run()方法中
-
通过Thread类创建线程对象,并将Runnable接口的子类对象作为构造函数的参数进行传递
因为线程的任务都封装在Runnable接口子类对象的run()方法中,所以要在线程对象创建时就必须明确要运行的任务
调用线程对象的start()方法开启线程
如果该类已经继承了一个父类,想扩展功能为多线程,可以通过接口的形式完成
它的出现仅仅是将线程的任务进行了对象的封装
class Demo extends FuDemo implements Runnable{
private String name;
Demo(String name){
// super(name); // 给线程起个名
this.name = name;
}
// 覆盖接口中的run方法
public void run(){
show();
}
public void show(){
for(int i = 0; i < 20; i++){
System.out.println(name + "....." + i + getName());
}
}
}
class ThreadDemo{
public static void main(){
Demo d = new Demo();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start(); // 开启线程,调用run方法
t2.start();
System.out.println("hello zimo!");
}
}
实现Runnable接口的好处:
-
将线程的任务从线程的子类中分离出来,进行了单独的封装
按照面向对象的思想将任务封装成了对象
避免Java单继承的局限性,所以第二种常用
6、线程的四种状态
- CPU的执行资格:可以被CPU处理到,在处理的队列中排队
- CPU的执行权:正在被CPU进行处理
sleep方法需要指定睡眠时间,单位是毫秒。
一个特殊的状态:就绪。具备了执行资格,但是还没有获取资源
多线程示例:卖票
class Ticket extends Thread{
private static int num = 100; // 如果不用静态的 他就会每个线程有独立的100张票
public void run(){
while(num > 0){
System.out.println(Thread.currentThread().getName() + "..sale.."+num--);
}
}
}
class Ticket1 implements Runnable{
private int num = 100;
public void run(){
while(num > 0){
System.out.println(Thread.currentThread().getName() + "..sale.."+num--);
}
}
}
class TicketDemo{
public static void main(String[] args){
Ticket t1 = new Ticket();
Ticket t2 = new Ticket();
Ticket t3 = new Ticket();
Ticket t4 = new Ticket();
t1.start();
// t1.start(); // 多次启动会抛出异常
t2.start();
t3.start();
t4.start();
Ticket1 t = new Ticket1(); // 创建一个线程任务对象
Ticket1 tt1 = new Ticket1(t);
Ticket1 tt2 = new Ticket1(t);
Ticket1 tt3 = new Ticket1(t);
Ticket1 tt4 = new Ticket1(t);
tt1.start();
tt2.start();
tt3.start();
tt4.start();
}
}
7、线程安全问题
- 导致产生线程安全的原因:
- 多个线程在操作共享的数据
- 操作共享数据的线程代码有多条
- 当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算
- 解决方法:
- Java中使用同步代码块:
synchronized(对象){需要被同步的代码块}
- 同步函数:
pubilc synchronized void add(int num){}
- Java中使用同步代码块:
- 同步锁使用前提:同步中必须有多个线程,并且使用同一个锁。
-
同步的好处:
>;
-
同步的弊端:
class Ticket implements Runnable{
private int num = 100;
Object obj = new Object();
public void run(){
// Object obj = new Object(); //假设所在方法里,每个线程都有自己单独的锁,那么还是存在问题
while(num > 0){
synchronized(obj){
// synchronized(new Object()){ // err 相当于一个线程有一个独立的对象
System.out.println(Thread.currentThread().getName() + "..sale.."+num--);
}
}
}
}
class TicketDemo{
public static void main(String[] args){
Ticket t = new Ticket(); // 创建一个线程任务对象
Ticket tt1 = new Ticket(t);
Ticket tt2 = new Ticket(t);
Ticket tt3 = new Ticket(t);
Ticket tt4 = new Ticket(t);
tt1.start();
tt2.start();
tt3.start();
tt4.start();
}
}
8、线程同步函数示例
// 需求:两个储户,每个都到同一家银行,每次100,共三次
class Bank{
private int sumMoney;
// private Object obj = new Object();
public synchronized void add(int money){ // 同步函数
// synchronized(obj){
sum += sum;
try {Thread.sleep(10);}catch(InterruptedException e){}
System.out.println("sum = " + sum);
// }
}
}
class Cus implements Runnable{
private Bank b = new Bank();
for(int i = 0; i < 3; i++){
b.add(100);
}
}
class CusBankDemo{
public static void main(String[] args){
Cus c = new Cus();
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
t1.start()
}
}
-
验证同步函数的锁
同步函数锁使用的是this对象
class Ticket implements Runnable{
private int num = 100;
// Object obj = new Object();
public void run(){
if(flag){
while(true){
// synchronized(obj){ // 如果是obj,那么线程代码块和线程函数的锁不是同意把,还是存在安全问题
synchronized(this){
System.out.println("this:" + this);
if(num > 0){
System.out.println(Thread.currentThread().getName() + "..sale.."+num--);
}
}
}
}else
while(true)
show();
}
public synchronized void show(){
// public static synchronized void show(){ // 静态代码块没有this对象,只有一个getClass获取的当前class字节码所属的对象
// 同步代码块可以通过传synchronized(this.getClass()){}来实现同步
// 获取字节码文件对象还可以通过类型.class: Ticket.class
System.out.println(Thread.currentThread().getName() + "..sale.."+num--);
}
}
class TicketDemo{
public static void main(String[] args){
Ticket t = new Ticket(); // 创建一个线程任务对象
System.out.println("t:" + t);
Ticket tt1 = new Ticket(t);
Ticket tt2 = new Ticket(t);
tt1.start();
tt2.start();
}
}
-
同步函数和同步代码块的区别是:
同步函数的锁是固定的this
同步代码块的锁是任意的对象
-
静态的同步函数使用的锁是,该函数所属字节码文件,该同步锁对象不是this
可以通过getClass()方法获取,也可以用当前
类名.class
表示
建议使用同步代码块
9、多线程下的单例模式
- 饿汉式(单例模式)
class Single{
private static final Single s = new Single(); // 1.固定不变,一开始就被创建
private Single(){}
public static Single getInstance(){
return s; // 2.返回地址,不存在线程安全问题
}
}
- 懒汉式(延迟加载单例模式)
class Single{
private static final Single s = null; // 共享数据
private Single(){}
public static Single getInstance(){
// 方法一:虽然解决了问题,但是每次进来都要判断锁,效率低
// public static synchronized Single getInstance(){
// 方法二:同步代码块,这样写还是和同步方法没区别,一进来就要判断锁
synchronized(Single.class){ // 这边不能使用getClass()方法,是非静态的
if(s == null){
// 可能存在线程切换同时进入这里
s = new Single(); // 产生多个对象,不能保证唯一性
}
}
return s;
}
// 改良,通过双重判断解决懒汉式的 线程安全问题 和 效率问题
public static Single getInstance(){
if(s == null){
synchronized(Single.class){
if(s == null){
s = new Single();
}
}
}
return s;
}
}
10、死锁示例
死锁:常见的情景之一:同步的嵌套
class Ticket implements Runnable{
private int num = 100;
private boolean flag = true;
Object obj = new Object();
public void run(){
if(flag){
while(true){
synchronized(obj){ // obj -->> this
show();
}
}
}else
while(true)
show();
}
public synchronized void show(){ // this -->> obj
synchronized(obj){
System.out.println(Thread.currentThread().getName() + "..sale.."+num--);
}
}
}
class DeadLockDemo{
public static void main(String[] args){
Ticket t = new Ticket(); // 创建一个线程任务对象
Ticket t1 = new Ticket(t);
Ticket t2 = new Ticket(t);
t.flag = false;
t1.start();
t2.start();
}
}
- 手动实现死锁示例:遵循原则--嵌套
class Test implements Runnable{
private boolean flag;
Test(boolean flag){
this.flag = flag;
}
public void run{
if(flag){
synchronized(MyLock.locka){
system.out.println("if-locka");
synchronized(MyLock.lockb){
system.out.println("if-lockb");
}
}
}else{
synchronized(MyLock.lockb){
system.out.println("else-locka");
synchronized(MyLock.locka){
system.out.println("else-lockb");
}
}
}
}
}
class MyLock{
public static final Object locka = new Object();
public static final Object lockb = new Object();
}
class DeadLockTest{
public static void main(String[] args){
Test a = new Test(true);
Test b = new Test(false);
Thread t1 = new Thread(a);
Thread t2 = new Thread(b);
t1.start();
t2.start();
System.out.println("hello zimo!");
}
}