1-概念
即使是单核的cpu操作系统也是轮流让多个任务交替执行,例如,让浏览器执行0.001秒,让QQ执行0.001秒,再让音乐播放器执行0.001秒,在人看来,CPU就是在同时执行多个任务,即使是多核CPU,因为通常任务的数量远远多于CPU的核数,所以任务也是交替执行的.
进程:如果采用单进程模式的话,那么我们一般把一个运行的游览器或者QQ又或者Word等称为一个进程.
线程:比如Word有打印-检查-保存等功能,我们把每一个子功能叫线程
进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。
2-进程VS线程
多线程:一个软件只存在一个进程,它的所有子功能都采用线程模式.
多进程:一个软件存在多个进程,没有线程.
多进程多线程:一个软件存在多个进程,每个进程还包含多个线程.
优缺点:多进程模式开销较大,但是稳定,一个进程出了问题不影响其他进程.多线程模式开销小,但是一个线程出了问题,整个进程就down掉.
JAVA:一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法内部,我们又可以启动多个线程.
3-Java线程的创建
public class xiancheng {
//系统创建的第一个线程main线程
public static void main(String[] args) {
System.out.println("main is start");
//新增MyThread线程,有多种方法,这里采用实现接口的方式
Thread t1 = new Thread(new MyThread());
//新进程启用
t1.start();
System.out.println("main is end");
}
}
//通过实现Runnable接口里面的run方法
class MyThread implements Runnable{
@Override
public void run() {
System.out.println("MyThread is start");
System.out.println("MyThread is end");
}
}
说明:上述代码中,一共创建了两个线程一个mian线程,一个MyThread线程,同一个线程代码的执行顺序是从上往下.当执行完"main is start"后,JVM创建了一个新线程MyThread,此时存在两个线程,系统会分批次的随机执行两个线程里面的代码,所以我们就不知道"main is end"是在"MyThread is start"和"MyThread is end"哪个位置执行了.
4-线程的状态
New: 新创建的线程,尚未运行.
Runnable:正在运行
blocked:因为某些操作而堵塞
watting:因为某些操作而等待
Timed Waiting:因为执行sleep()而等待
Terminated:线程终止,run()方式执行完毕
5-join()和interrupt
join():一个进程结束以后另一个进程才继续执行下面的代码
interrupt:中断线程,只是向线程发送中断信号,线程将isInterrupted属性设置为false,需要自己在run()方法中进行逻辑代码.
public class xiancheng {
//创建第一个新线程main线程
public static void main(String[] args) throws InterruptedException {
System.out.println("main线程开始");
//新增MyThread线程,有多种方法,这里采用继承的方式
Thread t1 = new MyThread();
t1.start();//新进程启用
Thread.sleep(10);//线程暂停10毫秒
t1.interrupt();//向t1线程发送中断信号,在run()方法里面通过判断isInterrupted来做处理.
t1.join();//等待t1线程结束后,main线程再进行
System.out.println("main线程结束");
}
}
//通过实现Runnable接口里面的run方法
class MyThread extends Thread{
@Override
public void run() {
int n = 0;
//通过判断isInterrupted来确定是否发送了中断信号
while (!isInterrupted()) {
n++;
System.out.println("n="+n);
}
}
}
中断线程另外一个方法:通过添加一个标记
public class XCInterrupt {
public static void main(String[] args) throws InterruptedException {
MyThread2 t = new MyThread2();
t.start();
MyThread2.sleep(10);
t.running = false;//run()方法中通过判断running=false结束进程
}
}
class MyThread2 extends Thread{
volatile boolean running = true;//定义标记字段running,必须用volatile修复
int n = 0;
@Override
public void run() {
while (running) {
System.out.println(n++);
}
System.out.println("中断进程");//当running = false线程代码执行结束.
}
}
6-多线程同步问题
当多个线程同时执行的时候,操作系统来自动分配什么时候执行每个线程里面的哪段指令代码,比如A和B线程同时执行,操作系统可能会先执行线程A中的第一段代码,然后跑过去执行B线程的第一段代码,然后又跑过去执行A的第二段指令代码.那么如果A和B线程有一个共享变量,那就可能会出现数据不一致的情况,
public class XCProblem1 {
public static void main(String[] args) throws InterruptedException {
ThreadA threadA = new ThreadA();
ThreadB threadB = new ThreadB();
threadA.start();
threadB.start();
threadA.join();
threadB.join();
System.out.println("Count=" + Counter3.count);
}
}
class Counter3 {
public static int count = 100;
}
class ThreadA extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
Counter3.count++;
}
}
}
class ThreadB extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
Counter3.count--;
}
}
}
静态变量:所谓静态变量是JVM初始化的时候实例化的,整个JVM只有一个静态变量Counter3.count,不管是多线程还是不同对象之间,只要有一个改变了静态变量的值,全局都会变.静态变量是跟随类的,非静态变量是跟随实例化对象的.
静态变量赋值过程:读取变量的值(ILAOD)-----放到临时区对变量进行操作(IADD)----回写变量的值到JVM(ISTORE)
问题说明:上述代码中线程ThreadA和ThreadB共享静态变量Counter3.count,两个线程同时运行,结果每次并不等于100.原因是线程ThreadA读取变量的值100以后,系统没有往下继续执行,而是执行了线程ThreadB读取变量100的指令,这样两个线程读取到变量的值一样都是100,ThreadA操作+1得到结果101写入JVM,ThreadB操作-1得到结果99写入JVM,结果编程了99.
这说明多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等
7-多线程同步问题解决方案-synchronized
为了保证多线程中一组指令的原子性,可以采用锁的方式解决.
锁住对象的方式: synchronized(任意一个对象){},不同线程之间锁住同一个对象,指令执行完自动释放锁.其他线程如果发现此对象正处于锁住状态,指令也不会执行,这样就保证了一组指令只能被一个线程用.但是却影响了效率.
public class XCProblem1 {
public static void main(String[] args) throws InterruptedException {
ThreadA threadA = new ThreadA();
ThreadB threadB = new ThreadB();
threadA.start();
threadB.start();
threadA.join();
threadB.join();
System.out.println("Count=" + Counter3.count);
}
}
class Counter3 {
public static int count = 100;
static Object lock = new Object();
}
class ThreadA extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
synchronized (Counter2.lock) {
Counter3.count++;
}
}
}
}
class ThreadB extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
synchronized (Counter3.lock) {
Counter3.count--;
}
}
}
}
8-JAVA的原子性和可见性
参看资料:https://my.oschina.net/StupidZhe/blog/1578183
CPU执行:一个CPU在同一时刻只能执行一条指令,我们视觉上看到的同时执行多线程,是由CPU执行线程A一部分转过去执行线程B,轮询执行的.CPU不可能同时执行多条指令,当然多核CPU就另说了.
原子性: 在JAVA中,基本类型和引用类型直接赋值的指令是原子性的如 int i = 100
有人会问,就一条语句怎么看都是一步操作的,但是如果cpu处理4字节32位的0和1的时候,先处理前16位再处理后16位,当处理到前16位的时候线程中断了,那i最后的结果可就不一定是100了,所以原子性和数据库里面的事务一个道理,要么这条语句执行完,要么就不执行.java中直接赋值是一步操作完成的
但是i++这条语句就是非原子性的,因为在java中分两步完成的,先读取i的值,然后对i进行操作,如果两个线程同时运行,第一个线程读取完i的值后还没有进行赋值操作,另外一个线程就读取了i的值,这时候数据就不是我们想要的.
可见性:我们试想一下,当CPU总是去访问物理内存去获取变量,然后频繁地去修改物理内存上的值,是不是太麻烦了?这将导致CPU花大部分的时间在获取和修改物理内存的值。所以,现在的每个CPU内部都有它自己的内存空间,我们称之为 “CPU缓存” 。在Java中,解决可见性问题的方法就是在你所需多线程访问的变量在添加volatile关键词.当加入这个关键词后,多核CPU的情况下,给一个变量赋值就会告送CPU不要放到缓存里面了,直接放到物理内存中,这样多个CPU共享数据就不会出现问题.
9-synchronized修饰方法和修饰静态方法
当我们锁住的是this实例时,实际上可以用synchronized修饰这个方法。下面两种写法是等价的
public void add(int n) {
synchronized(this) { // 锁住this
count += n;
} // 解锁
}
public synchronized void add(int n) { // 锁住this
count += n;
} // 解锁
因此,用synchronized修饰的方法就是同步方法,它表示整个方法都必须用this实例加锁。
我们再思考一下,如果对一个静态方法添加synchronized修饰符,它锁住的是哪个对象?
public synchronized static void test(int n) {
...
}
对于static方法,是没有this实例的,因为static方法是针对类而不是实例。但是我们注意到任何一个类都有一个由JVM自动创建的Class实例,因此,对static方法添加synchronized,锁住的是该类的class实例。上述synchronized static方法实际上相当于
public class Counter {
public static void test(int n) {
synchronized(Counter.class) {
...
}
}
}
10-线程安全
下列代码
public class Counter {
private int count = 0;
public void add(int n) {
synchronized(this) {
count += n;
}
}
public void dec(int n) {
synchronized(this) {
count += n;
}
}
public int get() {
return count;
}
}
这样一来,线程调用add()、dec()方法时,它不必关心同步逻辑,因为synchronized代码块在add()、dec()方法内部。并且,我们注意到,synchronized锁住的对象是this,即当前实例,这又使得创建多个Counter实例的时候,它们之间互不影响,可以并发执行
Counter c1 = new Counter();
Counter c2 = new Counter();
// 对c1进行操作的线程:
new Thread(() -> {
c1.add();
}).start();
new Thread(() -> {
c1.dec();
}).start();
// 对c2进行操作的线程:
new Thread(() -> {
c2.add();
}).start();
new Thread(() -> {
c2.dec();
}).start();
如果一个类被设计为允许多线程正确访问,我们就说这个类就是“线程安全”的(thread-safe)
11-可重入锁和死锁
重入锁:JVM允许同一个线程获取到一个锁对象后再次使用此锁.
下列代码中add方法获得Counter此实例的锁后,如果n>0,调用dec方法,dec方法也需要实例Counter的锁,因为是同一个线程所以是可以直接使用的.
//可重入锁
class Counter {
private int n = 0;
// synchronized方法代表锁对象是此实例对象(this),add和dec用的都是同一个锁对象
public synchronized void add(int n) {
if (n > 0) {
dec(n);
} else {
n++;
}
}
public synchronized void dec(int n) {
n--;
}
}
死锁:两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去,这就是死锁
class Counter2 {
private int n = 0;
public void add() {
synchronized (LockA.class) {
this.n++;
synchronized (LockB.class) {
this.n--;
}
}
}
public void dec() {
synchronized (LockB.class) {
this.n++;
synchronized (LockA.class) {
this.n--;
}
}
}
}