多线程基础
多线程是什么
在了解多线程之前 我们先来了解一下进程的含义:
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。----来自百度百科
我们可以打开自己电脑的任务管理器,查看 “进程” 窗口的数据
这里每一个正在运行的程序都可以被理解为“进程”。
了解了进程是什么,现在就可以解释线程了。
线程可以理解为每个进程中独立的子任务。就像我们使用音乐软件听歌一样,系统在执行播放任务的同时,还有下载音频文件线程、搜索曲目线程等等,这些线程在我们看来,就是在同步进行的。
但是又有一个新的问题出现了,使用多线程有什么好处呢?
在我们学习工作中,使用操作系统时,可以一边播放音乐,一边编辑文档,同时还可以在后台下载资料,正因为处理器在这些线程之间快速地切换,使得我们感觉他们就是在同步执行的,这就是多线程为我们带来的便利。
更直观的表达就是,待运行的任务有3个,执行时间分别是5秒,20秒,1秒。单任务的特点是同步执行,在单任务执行环境下执行时,回先执行任务一,5秒后执行任务二,再过20秒再执行任务三;而多任务运行时,CPU就会在三个任务间高速切换,使任务3不必等待25才能执行,系统效率大大提升,与单任务对应,使用多线程也就是使用了异步。
使用多线程
Java中实现多线程编程的方法共有两种:
- 继承Thread类(实现了Runnable接口)
- 实现Runnable接口
继承Thread类
Thread类有一个 run() 方法。使用继承Thread类的方式使用多线程编程,就 需要重写 这个方法,这个方法的内容就是该线程要完成的 任务 。
public class MyThread extends Thread {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
int time = (int) (Math.random() * 1000);
Thread.sleep(time);
System.out.println("run=" + Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上面的类就继承了Thread类,现在通过调用MyThread对象的 start() 方法就可以 启动线程。
public class Test {
public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.setName("myThread");
// b01_thread.run();
thread.start();
for (int i = 0; i < 10; i++) {
int time = (int) (Math.random() * 1000);
Thread.sleep(time);
System.out.println("main=" + Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
这里需要注意一下,我们启动线程是调用了Thread对象的 start() ,而不是直接调用我们刚刚实现的 run() 方法,这是为什么呢?
原来Thread类中的start()方法会通知“线程管理器”此线程已经准备就绪,可以调用run()方法开始运行了,这个过程就相当于对系统下达了一个通知,让系统安排时间来执行run()方法内的代码,也就启动了线程,具有 异步执行 的效果。
而调用run()方法的结果就截然不同,他不会通知“线程管理器”,处理器也不会专门另行安排时间空间执行该方法,而是由main()方法调用run()方法执行代码,是一个 同步执行 过程。
另外我们还需要注意一件事:
线程对象执行start()方法的顺序并非线程执行的顺序,线程真正的执行顺序是随机的:
public class MyThread extends Thread {
private int i;
public MyThread(int i) {
super();
this.i = i;
}
@Override
public void run() {
System.out.println(i);
}
}
public class Test {
public static void main(String[] args) {
MyThread t1 = new MyThread(1);
MyThread t2 = new MyThread(2);
MyThread t3 = new MyThread(3);
MyThread t4 = new MyThread(4);
MyThread t5 = new MyThread(5);
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
执行结果:
实现Runnanle接口
当我们的类已经使用了宝贵的唯一继承机会,我们就不得不使用这个方法来实现多线程了。
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
我们的Runnable实现类MyRunnable已经写好了,但是如何执行呢?
我们需要看一下Thread类的构造方法:
我们发现Thread的构造方法中,有2个可以传入一个Runnable接口,那就说明我们可以通过Thread构造方法将我们的MyRunnable类传递进去,然后通过新的Thread实例来启动线程。
public class Test {
public static void main(String[] args) {
Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable, "b02_runnable");
thread.start();
System.out.println("运行结束");
}
}
线程安全
我们先来了解一下,什么叫做“非线程安全”这个术语。
非线程安全也叫作线不程安全,是指多个线程对同一个对象中同一个属性进行操作时,出现值不同步的情况,进一步影响程序的下一步进行的现象。
这里用两个事例来说明非线程安全问题:
- 共享变量时引起的异常
- 自增/减与System.out.println()方法引起的异常
共享变量时引起的异常
在试验中,我们不难发现,使用new Thread()得到的多个对象之间的属性是独有的,互不干涉,但如果我们需要多个线程都可以修改这个值时,我们就需要使用 new Thread(Runnable runnable) 和 new Thread(Runnable runnable, String name) 两个构造方法来创建线程对象。
public class MyThread extends Thread{
private int count = 5;
// 存在安全隐患
@Override
public void run() {
super.run();
count--;
System.out.println(this.currentThread().getName() + "进行计算:" + count);
}
}
public class Test {
public static void main(String[] args) {
MyThread thread = new MyThread();
Thread t1 = new Thread(thread, "t1");
Thread t2 = new Thread(thread, "t2");
Thread t3 = new Thread(thread, "t3");
Thread t4 = new Thread(thread, "t4");
Thread t5 = new Thread(thread, "t5");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
我们发现在修改count的时候,产生了异常情况,这是因为我们的改值的执行过程中有多个步骤,但是多个线程同时访问这个值,自然会出现非线程安全问题。
我们这时候要引入一个关键词 synchronized .通过这个关键字,我们就可以对被修饰的方法或对象加上一道“锁”,加上锁的这部分代码被称作“互斥区”或“临界区”。一个线程只有拿到这把锁才可以执行被修饰的代码,而其他没有拿到这个锁的线程就会不断尝试去争抢这把锁,直到抢到为止。因此我们把MyThread类改成下面这样,就可以将对count的操作进行同步操作。
public class MyThread extends Thread{
private int count = 5;
// 存在安全隐患
@Override
synchronized public void run() {
super.run();
count--;
System.out.println(this.currentThread().getName() + "进行计算:" + count);
}
}
自增/减与System.out.println()方法引起的异常
这里讲述一下变量自增自减与System.out.println()方法共用时可能会引起的一种情况。
public class MyThread extends Thread {
private int i = 1;
@Override
public void run() {
System.out.println("i=" + (i++) + "\nthread -- " + Thread.currentThread().getName());
}
}
public class Test {
public static void main(String[] args) {
MyThread thread = new MyThread();
Thread t1 = new Thread(thread);
Thread t2 = new Thread(thread);
Thread t3 = new Thread(thread);
t1.start();
t2.start();
t3.start();
}
}
在博主启动了无数次程序之后,终于出现了罕见的非线程安全的问题。
这是因为自增操作是在System.out.println()方法执行前执行的。因此虽然System.out.println()是同步的,但是仍然会发生非线程安全问题。所以应该继续使用同步方法。