Java多线程(详细)

0. 进程与线程

1. jvm中的多线程机制-垃圾回收

下面举一个java程序中多线程机制的例子, 我们知道java拥有垃圾回收的机制, 而垃圾回收都是在我们"不知情"情况下发生的, 似乎与我们编写的程序走在两条不同的道上,jvm运行垃圾回收的代码并不会阻塞我们的代码运行, 而是和我们的代码"同时"执行的, 其实这就是多线程.

在jvm中 , 每个类在被回收时都会调用继承自Object类的finalize()方法, 我们可以做一个测试, 看他是不是多线程

2.1 finalize()方法

public  class Test {
	public static void main(String[] args) {
		for (int i = 0; i < 1000000; i++) {
			new GCDemo(i);
		}
	}
}

class GCDemo{
	final int i;
	public GCDemo(int i) {
		super();
		this.i = i;
	}
	@SuppressWarnings("deprecation")
	@Override
	protected void finalize() throws Throwable {
		super.finalize();
		System.out.println("Garbage Collecting"+i);
	}
}


这里@SuppressWarnings(“deprecation”), 的原因是JDK1.9 以后finalize就不推荐使用了, JDK1.9 以后可以实现AutoCloseable 接口, 使用CleanerAPI来进行垃圾回收, 但是代码较复杂, 这里为了方便演示, 还是使用finalize()方法

2.1 运行结果

运行结果如下, 显然, 这段代码并不是"按顺序执行的", 因为如果是的话, 那么数字应该是按顺序往下排, 现在对多线程多了些理解呢?别着急, 继续往下看
在这里插入图片描述

线程是Java 程序中程序执行的基本模型, Java语言和它的API为创建和管理线程提供了丰富的方法, 所有Java程序至少由一个控制线程组成(哪怕是只有空的main函数的Java程序,也是在JVM中作为一个线程运行的)

2 创建线程

Java中主要有两种创建线程的技术

  1. 创建一个新的类继承Thread类, 并且重载run()方法
  2. 定义一个 实现Runnable接口 的类, 并且实现run()方法

2.1 继承Thread类

该种方法主要包括三个步骤

  1. 继承Thread类
  2. 重写run()方法
  3. 调用start()方法

start()方法与run()方法

创建Thread 对象并不会创建一个新的线程, 实际上新的线程是由start()函数进行创建的

start函数会做下面两件事

  1. 在JVM中分配函数所需的内存并且分配进程
  2. 调用run()方法, 使线程在JVM中运行(不直接调用run()函数,而是调用start函数, 它再调用run()函数

如果你直接调用run() 方法, 它将会在main函数的线程中执行

示例代码

public  class Test{
	public static void main(String[] args) {
		MyThread mt = new MyThread();
		mt.start();
		for (int i = 0; i < 10; i++) {
			System.out.println("main"+i);
		}	
	}
}
class MyThread extends Thread{
	@Override
	public void run() {
		super.run();
	for (int i = 0; i < 10; i++) {
		System.out.println("Thread"+i);
	}		
	}
}

在这里插入图片描述

匿名内部类实现

Thread类可以使用匿名内部类实现

		new Thread() {
			public void run() {
				super.run();
			for (int i = 0; i < 100; i++) {
				System.out.println("Thread"+i);
			}		
			}
		}.start();

2.2 实现Runnable接口

实际上实现Runnable 接口是一种更加常用的方法, 为什么呢?因为java是不允许多线程, 但允许实现多个接口, 这意味着当我们想给某个类添加多线程的特性时, Runnbale会比Thread方便许多

start()方法

实际上, Runnable接口中并没有start()方法, 那么我们如何来创建一个线程呢?
事实上, 创建线程的任务我们总是交给Thread类来完成, 要想利用Runnable接口实现的类创建线程, 需要实例化一个Thread对象, 构造参数中为Runnbale的实例化对象
在这里插入图片描述

示例代码

public  class Stuxx {
	public static void main(String[] args) {
		MyRunnable mr = new MyRunnable(); 
		Thread mt = new Thread(mr);
		mt.start();
	}
}


class MyRunnable implements Runnable {
	public void run() {
		for (int i = 0; i < 100; i++) {
			System.out.println("Runnable"+i);
		}	
	}
}

在这里插入图片描述

★lambda表达式实现

仔细查看Runnable 接口的声明代码, 会发现Runnbale是一个函数式接口, 我们可以直接使用lambda表达式来创建它
在这里插入图片描述

public  class Test{
	public static void main(String[] args) {
	//使用匿名表达式创建线程
		new Thread(()->{
			for (int i = 0; i < 100; i++) {
				System.out.println("main"+i);
			}	
		}).start();
	}
}

3. 设置/获取线程名

我们可以利用线程名来区别每个不同的线程
默认线程名为 Thread-i,i从0开始计数

		Thread	t1=new Thread(()->{
			for (int i = 0; i < 100; i++) {
				System.out.println("main"+i);
			}	
		});
		System.out.println(t1.getName());  //输出Thread-0

3.1 在构造中设置

       Thread	t1 = new Thread("Thread1") {
			public void run() {
				super.run();
			for (int i = 0; i < 100; i++) {
				System.out.println("Thread"+i);
			}		
			}
		};
		
		System.out.println(t1.getName());

3.2 利用方法设置/获取

		Thread	t1=new Thread(()->{
			for (int i = 0; i < 100; i++) {
				System.out.println("main"+i);
			}	
		});
		t1.setName("myThread");
		System.out.println(t1.getName());

4. 守护线程(daemon thread)

什么是守护线程呢?, 举个象棋的例子, 帅或将就是其他的子的"守护进程", 当帅或将死去时, 这些守护进程都不复存在
也就是说, 当其他进程执行完毕时, 守护线程结束运行

4.1 设置守护线程

使用下面语法设置守护进程

Thread实例化对象.setDaemon(true);

4.2 示例代码

	public static void main(String[] args){
		Thread tb = new Thread() {
			public void run() {
				super.run();
			for (int i = 0; i < 50; i++) {
				System.out.println("ThreadDaemon"+i);
			}		
			}
		};
		tb.setDaemon(true);
		Thread ta = new Thread() {
			public void run() {
				super.run();
			for (int i = 0; i < 2; i++) {
				System.out.println("ThreadA"+i);
			}		
			}
		};
		tb.start();
		ta.start();
	}

可以看到本该执行50次的守护进程提前结束
在这里插入图片描述

5. 加入线程

所谓加入线程, 其实可以理解为插队, 比如有t1, t2两个线程, 当我调用t2.join()加入线程时, t2就插了t1 的队,此时t1立即停止执行, 等t2执行完了, 再执行t1

5.1 直接加入

	public static void main(String[] args){
		Thread tb = new Thread() {
			public void run() {
				super.run();
			for (int i = 0; i < 20; i++) {
				System.out.println("ThreadBBB"+i);
			}		
			}
		};
		
		
		Thread ta = new Thread() {
			public void run() {
				super.run();
			for (int i = 0; i < 20; i++) {
				try {
					if (i ==10) {
						tb.join();
					}
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				System.out.println("ThreadAAA"+i);
			}		
			}
		};
		ta.start();
		tb.start();
	}

5.2 加入一段时间

可以给join传递一个参数(单位为毫秒), 表示我只插 这么多毫秒的队伍

	public static void main(String[] args){
		Thread tb = new Thread() {
			public void run() {
				super.run();
			for (int i = 0; i < 20; i++) {
				System.out.println("ThreadBBB"+i);
			}		
			}
		};
		Thread ta = new Thread() {
			public void run() {
				super.run();
			for (int i = 0; i < 20; i++) {
				try {
					if (i ==2) {
						tb.join(30);
					}
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				System.out.println("ThreadAAA"+i);
			}		
			}
		};
		ta.start();
		tb.start();
	}

可以看到线程A先执行了一会, 到达2时立即停止交给B执行30毫秒, 之后退出"插队"

在这里插入图片描述

6. 锁

有些代码我们希望它不要与某些代码进行多线程的执行,或者这段代码一次只能由一个线程执行(线程安全性), 这时候就可以使用锁来同步代码块, 比如有代码块1 , 代码块2, 上了同样的锁之后, 代码块1执行时 就不会切换到代码块2执行

锁有以下几个关键概念, 先混个眼熟

  • 如果想要同步代码块, 必须使用同样的锁
  • 使用synchronized声明同步方法时,锁为this
  • 使用synchronized声明静态同步方法时,锁为字节码对象(即对象名.class)
    下面我们使用以下代码来做示例, 下面代码未使用锁, print1print2同时执行
class Printer{
	void print1() {
		for(int i = 0 ; i< 10000;i++)
		{
			System.out.println(1111111);
		}
	}
	void print2() {
		for(int i = 0 ; i< 10000;i++)
		{
			System.out.println(2222222);
		}
	}
}
	public static void main(String[] args){
		Printer p = new Printer();
		new Thread(()->{
			p.print1();
		}).start();;
		new Thread(()->{
			p.print2();
		}).start();;
	}

很明显, 输出结果是print1 和print2 交替执行的结果

6.1 使用相同的锁

现在我们给代码加上锁, 可以看到print1 与print2不再交替执行, 注意要点必须使用相同的锁才能同步代码块, 而锁可以是任意的对象

class Printer{
	Object myLock = new Object();
	void print1() {
		synchronized (myLock) {
			for(int i = 0 ; i< 100;i++)
			{
				System.out.println(1111111);
			}
		}

	}
	
	void print2() {
		synchronized (myLock) {
			for(int i = 0 ; i< 100;i++)
			{
				System.out.println(2222222);
			}
		}
		

	}
}

6.2 直接使用关键字给方法加锁

我们可以给方法添加 synchronized 关键字来给方法添加锁, 之前的要点2已经说过, 这种情况下, 锁对象为this

但要注意的是, 使用这种方法的锁对象为this , 如果我们有两个实例化对象, 每个对象调用他们自己的同步方法时, 使用的是他们自己的this, 他们之间并不是"互锁" 的, 因为他们的锁对象是不同的, 如果我们希望把整个类锁住, 而不是对象, 可以考虑6.3 中的字节码对象锁

class Printer{
	Object myLock = new Object();
	synchronized void  print1() {
		
		for(int i = 0 ; i< 100;i++)
		{
			System.out.println(1111111);
		}
	}
	
	synchronized void print2() {
		for(int i = 0 ; i< 100;i++)
		{
			System.out.println(2222222);
		}
	}
}

6.3 使用字节码对象给方法加锁

上面说到我们可以使用字节码对象类名.class的方式来给方法加锁, 这种方法有什么优点呢?

  • 总是同一个对象, 意味着总是同一个锁
  • 这个对象在执行前就已经存在了, 这也是为什么静态方法可以使用它来作为锁对象的原因

下面的print1 和print2 上锁是语法虽然不同, 但是本质是一样的, 因此具有相同的锁

class Printer{
	Object myLock = new Object();
	public static synchronized void  print1() {
		
		for(int i = 0 ; i< 100;i++)
		{
			System.out.println(1111111);
		}
	}
	
 void print2() {
	 synchronized (Printer.class) {
			for(int i = 0 ; i< 100;i++)
			{
				System.out.println(2222222);
			}
	}

	}
}

6.4 线程安全

既然有线程安全, 那什么是线程不安全呢?举个例子
当我们在买票的时候, 每个票都有唯一的号码对吗, 而其实每个用户向服务器发送请求时的线程是不一样的

  • 如果服务器处理这段请求是线程不安全的, 那么它在就可能出现这种情况 :用户一在买票时请求, 用户二也请求, 这时服务器同时处理数据, 导致他返回了相同的单号, 那么解决问题的关键是什么呢?

就是一次只处理一个请求, 在处理这个请求的时候, 不允许其他的线程处理请求, 我们用下面的程序示例
在不上锁时, 这段代码的ticket 将可能卖到 负数张而不会停止

//main方法
	public static void main(String[] args){
		new TicketSeller("1").start();
		new TicketSeller("2").start();
		new TicketSeller("3").start();
		new TicketSeller("4").start();
	}
class TicketSeller extends Thread{
	private static int ticket = 100;
	private String name;
	public TicketSeller(String name) {
		super();
		this.name = name;
	}
	@Override
	public void run() {
		while(true) {
			if(ticket == 0)
				break;
			System.out.println("Window"+this.name +"Selling ticket" + ticket);
			ticket--;
		}
	}
}

现在我们将其上锁解决线程不安全的问题, 也就是一次只能由一个线程执行该段程序

class TicketSeller extends Thread{
	private static int ticket = 100;
	private String name;
	public TicketSeller(String name) {
		super();
		this.name = name;
	}
	@Override
	public void run() {
		synchronized (TicketSeller.class) {
			while(true) {
				if(ticket == 0)
					break;
				System.out.println("Window"+this.name +"  Selling ticket" + ticket);
				ticket--;
			}
		}

	}
}


可以看到程序正常输出
在这里插入图片描述

6.5 死锁

什么是死锁呢? 从简单的从字面上理解, 其实就是线程被死锁锁住了, 动不了了, 其实也就是这么回事, 那么死锁产生的原因是什么呢?请看下面这段代码

class DeadLock extends Thread{
	
 private  static String  s1 = "线程1";
 private  static String  s2 = "线程2";
 	
 	
	
	void task1() {
		while(true) {
			synchronized (s1) {
				System.out.println("获取"+ s1+"等待" + s2) ;
				synchronized(s2) {
					System.out.println("获取到"+s2+"任务完成") ;
				}
			}
		}
	}
	
	void task2() {
		while(true) {
			synchronized (s2) {
				System.out.println("获取"+ s2+"等待" + s1) ;
				synchronized(s1) {
					System.out.println("获取到"+s1+"任务完成") ;
				}
			}
		}
	}
}
//main 方法
	public static void main(String[] args){
		DeadLock dl = new DeadLock();
		new Thread(()-> {
			dl.task1();
		}).start(); ;
		new Thread(()-> {
			dl.task2();
		}).start(); ;
		
	}

可以看到程序执行一段时间后就停止了运行, 原因是在某一时刻线程1 在等待线程2释放继续执行, 而线程2也在等待线程1 释放继续执行, 导致谁都无法继续执行下去, 这就是死锁
在这里插入图片描述
那么我们应该如何避免死锁呢? 很明显,死锁出现的原因是同步代码块的嵌套,因此, 我们应该减少同步代码块的嵌套

7. 进程间通信

有时候我们方法的执行有一定的顺序, 必须要方法1 执行完或者执行到某一行交给方法2 执行, 这是就需要用到进程间通信, 来告知方法2执行代码

7.1 wait()/notify()

进程间通信利用的是waitnotify方法, 这两个方法都是Object类的方法

  • wait(): 停止执行代码,释放锁并且等待notify()
  • notify(): 告知wait()的方法继续执行代码, 但不释放锁
  • 在同步代码块中锁对象是谁, 就用哪个锁对象来调用wait

之所以wait() notify()定义在Object中是因为所有的类都是Object的子类

7.2 两个进程间的通信

可以看到task1 task2交替执行

class TastRunner{
	private Object obj = new Object();
	private static  int flag = 1;
	void task1() throws InterruptedException {
		synchronized (obj) {

			while(true) {
				if(flag==2)
				{obj.wait(); }
				for(int i = 0 ; i<4;i++)
				{
					System.out.println("Task1-step"+i+"is going");
				}
				obj.notify();
				flag++;
			}
		}

	}
	
	void task2() throws InterruptedException {
		synchronized (obj) {
			while(true) {
				if(flag!=2)
				{obj.wait(); }
				for(int i = 0 ; i<4;i++)
				{
					System.out.println("Task2-step"+i+"is going");
				}
				flag=1;
				obj.notify();
			}
		}
	}
	
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值