学习一下入门级的多线程
学习多线程,先了解什么叫线程,什么叫进程
一般我们运行的程序都是进程级的,就是运行一个进程的意思,进程之下还有一群小弟叫线程,多个进程可以同时运作,多个线程也可以,但是一般我们只认识大哥,就是进程,所以用户一般不纠结这个,但是开发人员就不一样了,得知道,一个进程其实内部还有若干个线程,一般最少会有一个主线程,在java里对应的是main方法开启的main线程,这一段要强调的是,线程才是资源调度的最小单位,而不是进程。
线程与进程的区别
一般提到这个,应该主要比较的是多线程与多进程的区别
- 首先是资源调度,进程的开销会比线程大,大多少就得看用什么系统,对window来说貌似区别挺大的
- 通信,多线程通信比较容易,多进程通信比较复杂,多线程通信很快,多进程比较慢
- 多进程稳定性比较好,一个进程的崩溃不会影响到其他进程,多线程稳定性较差,一个线程崩了整个进程就崩了也就是说所有线程都崩了
大概就是这么多吧,以后还有再补
并发与并行
上面说到多进程多线程的同时运作,本质上是不对的,首先要了解到计算机的发展是从单核到多核,但是即使是单核时期,也能实现所谓的多任务同时进行,前提是我们使用的都是支持多任务的系统,当有运行多任务的请求时,cpu会分别分配给各个任务一段运行时,让各个任务交替进行,因为这个运行时很短,所以一般我们感觉不到,比如,让浏览器执行0.001秒,让QQ执行0.001秒,再让音乐播放器执行0.001秒,在人看来,CPU就是在同时执行多个任务。即使是多核CPU,因为通常任务的数量远远多于CPU的核数,所以任务也是交替执行的。顺便提一下,我们使用的windows,linux都是抢占式多任务系统。那么这就是并发。
并行就不一样了,并行是指多个任务,拥有独立的运行时,独立的CPU,互不干涉,是真正的同时进行,多核时代才能做到这种程度,但是一般来说这样太浪费资源,多数情况下我们使用的都是并发,常提到的高并发。
2019/12/19:
并发:同一时间段,多个任务都在执行(参考上面的描述)
并行:单位时间内,多个任务同时执行
新建java线程
有两种方式开启一个线程,一是继承一个Thread类:
public class Test{
public static void main(String args[]){
Thread thread = new Mythread();
thread.start();
}
}
public class MyThread extends Thread{
public void run(){
System.out.println("hello thread!");
}
}
或者是实现Runnable接口
public class Test{
public static void main(String args[]){
Thread thread = new Thread(new Mythread());
thread.start();
}
}
class MyThread implements Runnable{
public void run(){
System.out.println("hello thread!");
}
}
注意两种方法都一定要有run(),因为这是java线程主要执行方法,类似于main,第一种方法可以使用父类Thread实现的一些方法,比如interrupt()等等,继承后要重写run(),第二种比较简易,就一个run(),如果你不用不上父类的方法,一般推荐使用Runnable,叫轻量级的线程,继承了Thread会略显冗余。还有不要忘了调用thread实例的start(),否则线程不运行!
2019/12/19 尽量不要用单元测试调试,就是junit,因为他太强了,甚至能避免因为死锁或死循环导致的严重失误,那可不太好,还是老老实实再main方法里运行吧。
线程状态
在Java程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:
- New:新创建的线程,尚未执行;
- Runnable(Running):运行中的线程,正在执行run()方法的Java代码;
- Blocked:运行中的线程,因为某些操作被阻塞而挂起(比如:锁);
- Waiting:运行中的线程,因为某些操作在等待中;
- Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
- Terminated:线程已终止,因为run()方法执行完毕。
大概是这样
2019/12/19
上面添加了一个Running,根据大神的说法,这里是因为操作系统隐藏了jvm的Running和Runnable状态,所以java只能看到runnable,但是这是两个不同的状态,new是初始化,Runnable是就绪态,然后才到running是运行态
线程终止:
- 运行完代码块的内容正常终止
- 代码运行出错异常终止
- 调用stop()函数强行终止(不建议)
线程中断
注意继承自Thread的线程类才有判断中断的方法
类似这样的
class MyThread extends Thread {
public void run() {
int n = 0;
while (! isInterrupted()) {
n ++;
System.out.println(n + " hello!");
}
}
}
在别的线程里调用该线程的interrupt()方法,isInterrupted()就会返回false跳出循环,类似下载,取消下载的时候就可以这样中断线程
还有一种情况,如果在线程等待时发出了中断请求,线程就会抛出InterruptedException异常,这时候捕捉到这个异常就意味着收到中断请求,视情况决定,一般是要结束的。比如:
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
Thread.sleep(1000);
t.interrupt(); // 中断t线程
t.join(); // 等待t线程结束
System.out.println("end");
}
}
class MyThread extends Thread {
public void run() {
Thread hello = new HelloThread();
hello.start(); // 启动hello线程
try {
hello.join(); // 等待hello线程结束
} catch (InterruptedException e) {
System.out.println("interrupted!");
}
hello.interrupt();
}
}
class HelloThread extends Thread {
public void run() {
int n = 0;
while (!isInterrupted()) {
n++;
System.out.println(n + " hello!");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
}
}
上面的情况就是,t本身调用了join等待内部线程执行,这时候main发出了中断请求,t捕捉到异常中断hello,自己也结束了
注意:这个join的用法就是阻塞自己,等待线程实例运行完毕在继续运行,而不是阻塞改实例
还有一种方法设立一个同步标志位
public class Main {
public static void main(String[] args) throws InterruptedException {
HelloThread t = new HelloThread();
t.start();
Thread.sleep(1);
t.running = false; // 标志位置为false
}
}
class HelloThread extends Thread {
public volatile boolean running = true;
public void run() {
int n = 0;
while (running) {
n ++;
System.out.println(n + " hello!");
}
System.out.println("end!");
}
}
volatile 表示这个变量是一个线程共享变量,确保每个线程都能读取到更新后的变量值,在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!
这会导致如果一个线程更新了某个变量,另一个线程读取的值可能还是更新前的。例如,主内存的变量a = true,线程1执行a = false时,它在此刻仅仅是把变量a的副本变成了false,主内存的变量a还是true,在JVM把修改后的a回写到主内存之前,其他线程读取到的a的值仍然是true,这就造成了多线程之间共享的变量不一致。
因此,volatile关键字的目的是告诉虚拟机:
- 每次访问变量时,总是获取主内存的最新值;
- 每次修改变量后,立刻回写到主内存。
守护线程
如果我们在main方法里开启了一个计时的线程,然后main方法结束了,但是还有这个计时线程,必须要等这个线程结束才能退出程序,或者线程出现死循环,必须中断,或者强制停止。解决方案就是使用守护线程。顾名思义,守护线程守护的是除了它自己以外的所有线程,如果除了它以外别的线程都结束了,jvm就不会等待守护线程,直接退出程序。可以这样设置:
Thread t = new MyThread();
t.setDaemon(true);
t.start();
线程同步
完成线程同步要使用到关键字 synchronized ,表示同步标记。线程同步是多线程最困难的部分。
当多线程之间需要同时读写共享变量时,会出现数据不一致的问题,这个时候就需要线程同步。通用的解决方案都是采用锁的方式,即一个线程通过一个共享实例获取变量前得到锁,别的线程无法获取这个变量的锁也无法解锁,等有锁的线程执行完相关操作后就会释放锁,这样其他线程就可以获取锁在执行相关的操作了,synchronized就是这样一个互斥锁,synchronized 用法如下:
synchronized(lockobject){
...statement...
}
lockobject 是一个共享实例,注意必须是一个共享的实例对象,所有的线程锁住一个相同的对象才能完成同步,正确用法:
public class SynchronizedTest1 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Thread t1 = new Hi();
Thread t2 = new Hel();
t1.start();
t2.start();
System.out.println(Count.i);
}
}
class Count{
public static Object key1 = new Object();
public static Object key2 = new Object();
public static int i= 0;
}
class Hi extends Thread{
public void run() {
synchronized(Count.key1) {
Count.i+=1000;
}
}
}
class Hel extends Thread{
public void run() {
synchronized(Count.key1) {
Count.i-=1000;
}
}
}
错误
class Hi extends Thread{
public void run() {
synchronized(Count.key1) {
Count.i+=1000;
}
}
}
class Hel extends Thread{
public void run() {
synchronized(Count.key2) {
Count.i-=1000;
}
}
}
没有锁住同一个对象,数据不一致,同步失败
synchronized也可以写在方法上,这时候锁住的是this实例,如下
public synchronized void add(int n) {
if (n < 0) {
dec(-n);
} else {
count += n;
}
}
public synchronized void dec(int n) {
count += n;
}
这就是同步方法
上面的代码有一个问题,在同一个实例的同步方法里调用另一个同步方法,不会出错么,对就是不会,因为java的synchronized锁是一种可重入的锁,或者嵌套的锁,因为是被同一个线程获取的所有不会有问题,那么这里要注意的就是死锁的问题了
死锁的意思就是线程获取了一个锁后在获取另一个锁
public void set(int m) {
synchronized(lockA) { // 获得lockA的锁
this.value += m;
synchronized(lockB) { // 获得lockB的锁
this.another += m;
} // 释放lockB的锁
} // 释放lockA的锁
}
public void get(int m) {
synchronized(lockB) { // 获得lockB的锁
this.another -= m;
synchronized(lockA) { // 获得lockA的锁
this.value -= m;
} // 释放lockA的锁
} // 释放lockB的锁
}
线程1执行set获取锁lockA,这个时候线程2也启动了执行get获取锁lockB,线程1想要lockB的锁,线程2想要lockA的锁,这个时候就陷入死循环了,这就是死锁,要极力避免这种情况出现。
一般很少采用同步方法的模式,如果锁住了整个实例,其他想要使用实例但是不涉及共享变量或者使用不同共享变量的线程就必须等待了,性能自然会下降
notify&wait
synchronized解决了线程同步的资源竞争问题,接下来要解决的就是线程协作的问题了,比如,一个缓存队列,我们希望多个线程写,队列不为空时读
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
}
public synchronized String getTask() {
while (queue.isEmpty()) {
}
return queue.remove();
}
}
这样写似乎没有什么问题,仔细看看,当队列为空,while进入死循环时,getTask持有this锁,并不能让出,那么写线程也执行不了,所以最终会陷入死循环,使用notify和wait方法改进一下:
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
this.notify();
}
public synchronized String getTask() {
while (queue.isEmpty()) {
this.wait();
}
return queue.remove();
}
}
wait()的作用就是进入等待状态,notify()的作用就是通知一个线程可以准备唤醒了,注意两个方法都必须在同步代码块中使用,并且由锁对象调用,这里锁是this所以是this调用的方法,另外,wait()实际上会释放线程持有的锁对象的锁,并进入等待,直到收到notify()的通知唤醒后会重新获取锁,wait是由native的代码实现的也就是c实现,原理很复杂,我不懂…,还有notify是唤醒任意一个等待线程,如果有多个等待的线程,会随机唤醒一个,一般不这么用,一般用notifyAll(),唤醒所有的等待线程,应该是为了避免活锁,活锁就是在资源竞争中一直无法获取资源无法执行的状态。由于notify不会导致阻塞,并且就算你提前用了,那唤醒的线程还是得等当前线程执行完毕,所以有时候需要提前执行或者可以提前执行。
while (queue.isEmpty()) {
this.wait();
}
这个地方要注意,用while是因为多个线程被唤醒后,进行资源竞争,如果使用的是if判断,这个时候获取锁就会直接执行读的方法,如果其中有一个线程读完了数据,那别的线程获取锁后也直接读,那读得就是空的,所以获取锁后一定要进行判断,再读数据,这是个特殊处理,不同的场景有不同的处理。
以下是找到的多线程的简单的练习,
多线程简单练习