JAVA-多线程从零开始第一篇

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--;
			}
		}
	}
}
发布了40 篇原创文章 · 获赞 4 · 访问量 6346

猜你喜欢

转载自blog.csdn.net/oFlying1/article/details/103668684