一.概念
1. 基本概念
1. 程序:
以某种编程语言编写,运行在计算机上,静态的执行固定功能。
2. 进程:
程序的一次执行过程(动态的);各线程相对独立;
3. 线程:
轻量级进程;相互转换耗资低;多个线程并行在单个进程上;多线程共享同一块内存空间和一组系统资源;线程是一个子任务,CPU会随机分配时间调用线程(run方法)
4. 并行:
同一时间同时执行
5. 并发:
宏观并行,微观轮执串休
2. 多线程:
几乎同一时间执行多个线程(一个处理器在某一极短时间内只能执行一个线程!即使该处理器多核,只不过线程间切换速度很快,多线程不规律的各执行一段时间,直到全部完成)
【类比多双手(线程)同时往嘴(此嘴吃得很快)里塞饭,加快了单手操作时嘴(CPU)的等待时间,嘴只能有一个,同时也要处理好多双手间的协调】
二. 实现线程的方式
1. 继承Thread类
定义好的实现了Runnable接口的类
(1)thread类在java.lang包中,一个类继承了这个类,就叫做多线程操作类,并且必须明确的覆写run()方法。
(2)调用start()方法才是真正的启动线程。
(3)java允许java程序访问操作系统的函数,在Thread的start()方法
中,有一个native关键字,表示的是一个由java调用本机操作系统函数的关键字。同时证明了,要想实现多线程,必须要获得操作系统的支持。
(4)sleep()方法,使线程休眠,时间为微秒
(5)join()方法使其他线程等待当前线程终止,等待时间为毫秒
(6)static void yield()方法,当前运行的线程释放处理器资源
(7)返回当前运行的线程引用。
public class MyThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("MyThread");
}
private void xxx() {
System.out.println("in xxx");
}
public static void main(String[] args) {
MyThread mythread1 = new MyThread();
MyThread mythread2 = new MyThread();
//注意:必须通过start()来启动线程,否则调用run()将仅仅是调用重写方法
mythread1.start();
mythread2.start();
//以下两句将先于线程被执行
mythread1.xxx();
mythread2.xxx();
//以下语句编译正常,运行时出错,同一对象不能重复start
// mythread1.start();
// mythread1.start();
}
}
2. 实现Runnable接口
(推荐选择继承接口,接口可多继承,也可以供类继承其他父类)
(1)实现runnable接口的类在创建线程时,通过Thread类的有参构造函数,创建线程,传入两个参数,第一个传入实现runnable接口的对象,第二个参数传入线程的名字。
public class MyRunnable implements Runnable {
@Override
public void run() {
//随机输出(A-Z)26个字母
for(int i=0;i<100;i++) {
System.out.println((char)(Math.random()*25+65));
}
}
public static void main(String[] args) {
//代理模式
Runnable runnable=new MyRunnable();
Thread thread=new Thread(runnable);
thread.start();
}
}
实例理解代理模式:
package java_20191222;
//代理模式:Thread类的代理机制
// 替你完成你想做的事情
public class Buyer implements BuyHouse {
@Override
public void buy() {
System.out.println("我呀买房子");
}
public static void main(String[] args) {
Buyer b = new Buyer();
Agent a = new Agent(b);
a.buy();
}
}
interface BuyHouse {
public void buy();
}
class Agent implements BuyHouse {
private Buyer b;
Agent() {}
Agent(Buyer b) {
this.b = b;
}
public void before() {
System.out.println("中介给你提供售前房源");
}
public void after() {
System.out.println("中介给你提供售后保障");
}
@Override
public void buy() {
before();
b.buy();
after();
}
}
- 深入比较Thread和Runnable接口
(1)从定义格式上,Thread实现了Runnable接口,当使用其他类mythread事项runnable接口时,相当于采用代理模式,Thread实现核心操作,mythread作为代理。
(2)Thread实现的线程不能共享资源,Runnable接口实现的线程可以共享资源。例如卖票因为Thread的子类中都有单独的票数变量,所以各卖各的。而Runnable不一样
(3)使用结论:使用Runnable接口比Thread类有以下优点:
适合多个线程共同处理一个资源,可以避免单继承带来的影响,增加了程序的健壮性。
三. 线程的状态
1. 出生态: 刚刚通过new创建
2. 就绪态: 已经start(),准备被CPU调用
3. 运行态: CPU分配时间运行中
4. 阻塞态:
(1)sleep():当前线程休眠
(2)join():等待当前线程终止后执行。相当于将该进程加入到当前线程的后面,成为同一条路径
(3)Scanner:键盘输入
(4)yield:暂停当前线程,让其他线程运行
5. 死亡态:
(1)该线程执行结束
(2)强行执行stop(),不推荐使用
· sleep()与join()的使用代码
public class ThreadDemo extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("线程1运行中:"+i+"%");
try {
//线程每执行一次休眠1秒
sleep(1000);
// System.out.println("睡一秒");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public static void main(String[] args){
ThreadDemo td=new ThreadDemo();
td.start();
ThreadDemo2 td2=new ThreadDemo2();
td2.start();
}
}
class ThreadDemo2 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// td2与td1并发到i=20,之后只运行td1到结束再运行td2
if(i==20) {
try {
join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("线程2运行中:"+i+"%");
try {
sleep(1000);
// System.out.println("睡一秒");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
/*部分执行结果
* ...
* 线程1运行中:17%
* 线程2运行中:17%
* 线程2运行中:18%
* 线程1运行中:18%
* 线程2运行中:19%
* 线程1运行中:19%
* 线程1运行中:20%
* 线程1运行中:21%
* 线程1运行中:22%
* ...
*/
//部分执行结果
...
线程1运行中:17%
线程2运行中:17%
线程2运行中:18%
线程1运行中:18%
线程2运行中:19%
线程1运行中:19%
线程1运行中:20%
线程1运行中:21%
线程1运行中:22%
...
四. 线程常用方法
Part 1
- currentThread()
返回对当前正在执行的线程对象 - getName()
返回当前线程对象名 - getId()
返回此线程的标识符 - getName()
返回此线程的名称 - getPriority()
返回此线程的优先级 - isAlive()
测试这个线程是否还处于活动状态(线程已经启动且尚未终止),线程处于正在运行或准备运行的状态 - sleep(long millis)
使当前正在执行的线程以指定的毫秒数“休眠”(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。 - interrupt()
中断这个线程。 - interrupted() 和isInterrupted()
interrupted():测试当前线程是否已经是中断状态,执行后具有将状态标志清除为false的功能
isInterrupted(): 测试线程Thread对相关是否已经是中断状态,但部清楚状态标志 - setName(String name)
将此线程的名称更改为等于参数 name 。 - isDaemon()
测试这个线程是否是守护线程。 - setDaemon(boolean on)
将此线程标记为 daemon线程或用户线程。 - join()
在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。
join()的作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行 - setPriority(int newPriority)
更改此线程的优先级,默认优先级(5)
Part 2 暂停线程
- yield()
放弃当前的CPU资源,将它让给其他的任务去占用CPU时间。注意:放弃的时间不确定,可能一会就会重新获得CPU时间片。 - stop()
这个方法不安全,不建议使用 - interrupt()
不会终止一个正在运行的线程,需要添加一个判断才可以完成线程的停止
五.线程优先级
每个线程都具有各自的优先级,线程的优先级可以在程序中表明该线程的重要性,如果有很多线程处于就绪状态,系统会根据优先级来决定首先使哪个线程进入运行状态。但这个并不意味着低 优先级的线程得不到运行,而只是它运行的几率比较小,如垃圾回收机制线程的优先级就比较低。所以很多垃圾得不到及时的回收处理。
线程优先级具有继承特性比如A线程启动B线程,则B线程的优先级和A是一样的。
线程优先级具有随机性也就是说线程优先级高的不一定每一次都先执行完。
Thread类中包含的成员变量代表了线程的某些优先级。如Thread.MIN_PRIORITY(常数1),Thread.NORM_PRIORITY(常数5), Thread.MAX_PRIORITY(常数10)。其中每个线程的优先级都在Thread.MIN_PRIORITY(常数1) 到Thread.MAX_PRIORITY(常数10) 之间,在默认情况下优先级都是Thread.NORM_PRIORITY(常数5)。
六. 线程共享数据
- 不共享数据的情况
public class MyThread extends Thread {
private int count = 5;
public MyThread(String name) {
super();
this.setName(name);
}
@Override
public void run() {
super.run();
while (count > 0) {
count--;
System.out.println("由 " + MyThread.currentThread().getName()
+ " 计算,count=" + count);
}
}
public static void main(String[] args) {
MyThread a = new MyThread("A");
MyThread b = new MyThread("B");
MyThread c = new MyThread("C");
a.start();
b.start();
c.start();
}
}
每个线程都有一个属于自己的实例变量count,他们之间的数据不相互影响
- 共享数据的情况
public class MyThread extends Thread {
private int count = 5;
@Override
public void run() {
super.run();
count--;
System.out.println("由 " + MyThread.currentThread().getName() + " 计算,count=" + count);
}
public static void main(String[] args) {
MyThread mythread=new MyThread();
//下列线程都是通过mythread对象创建的
Thread a=new Thread(mythread,"A");
Thread b=new Thread(mythread,"B");
Thread c=new Thread(mythread,"C");
Thread d=new Thread(mythread,"D");
Thread e=new Thread(mythread,"E");
a.start();
b.start();
c.start();
d.start();
e.start();
}
}
初步的通过同一对象创建多线程,未必能真正解决问题,因为难以避免同步问题
解决方案:线程同步—synchronized
七. 线程安全问题
1. 原因:
线程 数据是 栈独立, 堆共享 new 出来的对象 共享多个线程 访问一个资源 就会存在数据同步问题
2. 解决方法:
synchronized 同步: 多个操作在同一个时间段内只有一个线程进行,其他线程等待此线程完成之后才可以进行,想要实现有两种方法,使用同步代码块和同步方法。
(1)同步方法
使用synchronized关键字声明的方法叫做,同步方法
(2)同步代码块
使用synchronized关键字声明的代码块。同步的时候必须指定同步对象,一般使用当前对象,用this表示。
(注:线程同步将变慢执行速度)
3. 死锁问题:
(1)原因
资源共享时需要用到同步,过多的同步会造成死锁,死锁是程序在运行时产生的一种状态。
(2)解决方法
wait()
notifiy()
Object 类
八. synchronized使用
1. synchronized的三种应用方式
(1)修饰实例方法
作用于当前实例加锁,进入同步代码前要获得当前实例的锁
(2)修饰静态方法
作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
(3)修饰代码块
指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
2. synchronized作用于实例方法
所谓的实例对象锁就是用synchronized修饰实例对象中的实例方法,注意是实例方法不包括静态方法,如下
public class AccountingSync implements Runnable{
//共享资源(临界资源)
static int i=0;
//synchronized 修饰实例方法
public synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
AccountingSync instance=new AccountingSync();
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
// 输出结果: 2000000
}
上述代码中,我们开启两个线程操作同一个共享资源即变量i,由于i++操作并不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分两步完成,如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1操作,这也就造成了线程安全失败,因此对于increase方法必须使用synchronized修饰,以便保证线程安全。此时我们应该注意到synchronized修饰的是实例方法increase,在这样的情况下,当前线程的锁便是实例对象instance,注意Java中的线程同步锁可以是任意对象。从代码执行结果来看确实是正确的,倘若我们没有使用synchronized关键字,其最终输出结果就很可能小于2000000,这便是synchronized关键字的作用。这里我们还需要意识到,当一个线程正在访问一个对象的synchronized 实例方法,那么其他线程不能访问该对象的其他 synchronized 方法,毕竟一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized实例方法,但是其他线程还是可以访问该实例对象的其他非synchronized方法,当然如果是一个线程 A 需要访问实例对象 obj1 的 synchronized 方法 f1(当前对象锁是obj1),另一个线程 B 需要访问实例对象 obj2 的 synchronized 方法 f2(当前对象锁是obj2),这样是允许的,因为两个实例对象锁并不同相同,此时如果两个线程操作数据并非共享的,线程安全是有保障的,遗憾的是如果两个线程操作的是共享数据,那么线程安全就有可能无法保证了,如下代码将演示出该现象
public class AccountingSyncBad implements Runnable{
static int i=0;
public synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
//new新实例
Thread t1=new Thread(new AccountingSyncBad());
//new新实例
Thread t2=new Thread(new AccountingSyncBad());
t1.start();
t2.start();
//join含义:当前线程A等待thread线程终止之后才能从thread.join()返回
t1.join();
t2.join();
System.out.println(i);
}
}
上述代码与前面不同的是我们同时创建了两个新实例AccountingSyncBad,然后启动两个不同的线程对共享变量i进行操作,但很遗憾操作结果是1452317而不是期望结果2000000,因为上述代码犯了严重的错误,虽然我们使用synchronized修饰了increase方法,但却new了两个不同的实例对象,这也就意味着存在着两个不同的实例对象锁,因此t1和t2都会进入各自的对象锁,也就是说t1和t2线程使用的是不同的锁,因此线程安全是无法保证的。解决这种困境的的方式是将synchronized作用于静态的increase方法,这样的话,对象锁就当前类对象,由于无论创建多少个实例对象,但对于的类对象拥有只有一个,所有在这样的情况下对象锁就是唯一的。下面我们看看如何使用将synchronized作用于静态的increase方法。
3.synchronized作用于静态方法
当synchronized作用于静态方法时,其锁就是当前类的class对象锁。由于静态成员不专属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态 成员的并发操作。需要注意的是如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁,看如下代码
public class AccountingSyncClass implements Runnable{
static int i=0;
/**
* 作用于静态方法,锁是当前class对象,也就是
* AccountingSyncClass类对应的class对象
*/
public static synchronized void increase(){
i++;
}
/**
* 非静态,访问时锁不一样不会发生互斥
*/
public synchronized void increase4Obj(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
//new新实例
Thread t1=new Thread(new AccountingSyncClass());
//new心事了
Thread t2=new Thread(new AccountingSyncClass());
//启动线程
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
由于synchronized关键字修饰的是静态increase方法,与修饰实例方法不同的是,其锁对象是当前类的class对象。注意代码中的increase4Obj方法是实例方法,其对象锁是当前实例对象,如果别的线程调用该方法,将不会产生互斥现象,毕竟锁对象不同,但我们应该意识到这种情况下可能会发现线程安全问题(操作了共享静态变量i)。
4.synchronized同步代码块
除了使用关键字修饰实例方法和静态方法外,还可以使用同步代码块,在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了,同步代码块的使用示例如下:
public class AccountingSync implements Runnable{
static AccountingSync instance=new AccountingSync();
static int i=0;
@Override
public void run() {
//省略其他耗时操作....
//使用同步代码块对变量i进行同步操作,锁对象为instance
synchronized(instance){
for(int j=0;j<1000000;j++){
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
从代码看出,将synchronized作用于一个给定的实例对象instance,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有instance实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行i++;操作。当然除了instance作为对象外,我们还可以使用this对象(代表当前实例)或者当前类的class对象作为锁,如下代码:
//this,当前实例对象锁
synchronized(this){
for(int j=0;j<1000000;j++){
i++;
}
}
//class对象锁
synchronized(AccountingSync.class){
for(int j=0;j<1000000;j++){
i++;
}
}
5.底层原理
了解完synchronized的基本含义及其使用方式后,下面我们将进一步深入理解synchronized的底层实现原理。
synchronized底层语义原理
Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法 并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的,关于这点,稍后详细分析。下面先来了解一个概念Java对象头,这对深入理解synchronized实现原理非常关键。