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中主要有两种创建线程的技术
- 创建一个新的类继承
Thread
类, 并且重载run()
方法 - 定义一个 实现
Runnable
接口 的类, 并且实现run()
方法
2.1 继承Thread类
该种方法主要包括三个步骤
- 继承Thread类
- 重写run()方法
- 调用start()方法
start()方法与run()方法
创建Thread 对象并不会创建一个新的线程, 实际上新的线程是由start()
函数进行创建的
start函数会做下面两件事
- 在JVM中分配函数所需的内存并且分配进程
- 调用
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
)
下面我们使用以下代码来做示例, 下面代码未使用锁,print1
与print2
同时执行
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()
进程间通信利用的是wait
与notify
方法, 这两个方法都是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();
}
}
}
}