1、 线程与进程
什么是进程
程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。而进程是程序在处理器上的一次执行过程,它是一个动态的概念
进程是一个具有一定独立功能的程序,一个实体,每一个进程都有他自己的地址空间
进程的状态
进程执行的间断性,决定了进程可能具有多种状态。事实上,运行中的进程具有以下三种状态。
- 就绪状态(Ready)
- 运行状态(running)
- 阻塞状态(Blocked)
就绪 --进程调度算法–>执行, 执行–时间片用完–>就绪, 执行–IO事件发生–>阻塞, 阻塞 —事件结束–> 就绪
线程
线程实际上是在进程基础上进一步划分,一个进程启动后,里面若干程序可以划分成若干个线程。
线程:是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行,一个进程最少有一个线程。
并发 vs 并行
并发: 一个CPU, 并行:多个CPU
2、线程的基本使用
两种实现方法
继承Thread 类
class MyThread extends Thread{
public void run{}
}
MyThread mt = new MyThread();
mt.start();
实现runnalbe 接口
class MyRunnalbe implements Runnable{
public void run(){
}}
MyRunnable mr = new MyRunnable();
Thread t = new Thread(mr);
t.start();
实现 runnable 的类可以看作一个任务,而不是一个线程,我们可以把任务放在线程中,然后启动线程
3、线程休眠
public static void seep(long millis) throws InteruptedException
使正在执行的线程以指定的毫秒数暂停,释放CPU的时间片,具体取决于系统定时器和调度程序的精度和准确性。线程不会丢失任何显示器的所有权。
线程中断异常:线程只能由其自身关闭,不能由外部强行中断。
4、join与中断线程
public final void join() throws InterruptedException
等待这个线程死亡(让调用join() 的线程执行完毕后再执行其他的)
调用此方法的行为方式与调用完全相同
public void interrupt()
中断这个线程(设置一个中断状态(标记),不是真的中断)
除非当前线程中断自身,这是始终允许的
public static boolean interrupted()
测试当前线程是否中断,该方法可以清楚线程的中断状态,如果这个方法被连续调用两次,那么两个调用将返回false,(除非当前线程再次中断,在第一个调用已经清除其中断状态之后,在第二个调用之前已经检查过)
忽略线程中断,一位线程在中断时不存在将该方法返回false所反映
如何保证线程的执行顺序
public class ThreadInOrder {
public static void main(String[] args) {
testThread t1 = new testThread();
testThread t2 = new testThread();
testThread t3 = new testThread();
t1.start(); // 一号线程启动
try {
t1.join(); // 一号线程join
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start(); // 二号线程启动
try {
t2.join(); // 二号线程join
} catch (InterruptedException e) {
e.printStackTrace();
}
t3.start(); // 三号线程启动
try {
t3.join(); // 三号线程join
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
5、守护线程与yield
线程可以分为守护线程和用户线程,当进程没有用户线程时,JVM会退出, setDaemon(true)
setPriority : 优先级高可以提高线程抢占cpu时间片的概率
public static void main(String[] args) {
MyRunnable4 mr4 = new MyRunnable4();
Thread t = new Thread(mr4);
t.setDaemon(true);
System.out.println(t.isAlive());
t.start();
System.out.println(t.isAlive());
t.setPriority(Thread.MAX_PRIORITY); // 优先级高可以提高线程抢占cpu时间片的概率
for(int i = 0; i < 50; i ++){
System.out.println("main-"+i);
if( i ==5)
Thread.yield();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
6、并发编程的三个概念
原子性,可见性 有序性
原子性
原子性指一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行
分析以下代码,判断哪些是原子性的
x=10;//语句1
y=x;//语句2
x++;//语句3
x=x+1;//语句4
x=10,线程执行这个语句时直接把数值10写入工作内存
y=x,线程执行这个语句时,首先从工作内存中读取x的值,再将x的值写入到工作内存,虽然这两个操作都是原子性的,但是合起来就不是了。
x++,线程执行这个语句时,本质上执行了三个动作,先把x从工作内存中读取,在进行+1,再把最后的结果写入到工作内存。
也就是说,在java的内存模型中,只有对基本数据类型的简单读取和赋值是原子性的(相互赋值不是原子性)。
实现大范围原子性的方法:
悲观锁(synchronized或者Lock)
乐观锁(原子类 cas)
可见性
指多个线程操作一个共享变量时,其中一个线程对限量进行修改后,其他线程可以立即看到修改的结果,串行程序的可见性不存在,因为一定按照代码先后顺序。
当多个线程操作一个普通的共享变量时,其中某个线程操作的原理是: 首先从主内存中读取这个变量到自己的工作内存,在工作内存中修改以后保留变量的副本,在写入到主内存中,但是普通变量不存在可见性,当修改新值后,什么时候写入主内存是不一定的,当其他线程读取的时候,读取的可能还是旧值。
实现可见性的方法:
synchronized 或者lock: 保证同一时刻只有一个线程获取锁执行代码,锁释放之前把最新的值刷新到主内存,实现可见性。
volatile: 被volatile 修饰的变量, 一个线程修改后直接把值写入之内存,其他线程直接从主内存中读取
有序性
即程序的执行顺序按照代码的先后顺序来执行,
在Java 内存模型中,编译器和处理器允许对指令重新排序,在单线程下不会出现问题,在多线程会出现问题
实现有序性的方法: synchronized或者Lock volatile(禁止指令重排序)
7、线程同步
多线程共享数据
在多线程的操作中,多线程有可能同时处理同一个资源,这就是多线程中的共享数据
线程同步
解决数据共享问题,必须使用同步,所谓同步就是指多个线程在同一个时间段内只有一个线程执行指定代码,其他线程要等待此线程完成后才可以继续执行
实现同步有三种是实现方式
同步代码块
sybchronized(要同步的对象){ 要同步的操作; }
synchronized (this) {
if (ticket > 0) {
ticket--;
System.out.println("您购买的票剩余: " + ticket + "张");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
同步方法
public synchronized void method(){ 要同步的操作; }
private synchronized void method() {
ticket--;
System.out.println("您购买的票剩余: " + ticket + "张");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
解析synchronized 关键字:
在使用synchronized 关键字后,在字节码文件中有monitorenter 和monitorexit两个指令。monitorenter:每个对象都有一个监视器锁(monitor),当monitor 被占用时就会处于锁定状态,线程执行monitorenter命令获取monitor锁的过程如下:
- 如果monitor的进入数为0,则线程获取锁,并设置monitor的进入数为1
- 如果该线程已经占有该monitor,则进入数+1
- 如果其他线程占有该monitor,则monitor的进入数不为0,则该线程进入阻塞状态,直到monitor为0,重新获取monitor的所有权
monitorexit:执行monitorexit的线程必须是monitor的所有者。 当执行该命令时,monitor的进入数-1,当monitor的进入数为0,该线程已经不再是该monitor的所有者,其他被这个monitor阻塞的线程可以尝试获取monitor的所有权。
Lock(ReetrantLock)
ReentrantLock lock = new ReentrantLock();
private void method2() {
lock.lock();
try {
ticket--;
System.out.println("您购买的票剩余: " + ticket + "张");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}
构造器都有一个布尔参数 fair, 它允许你控制这两个类的行为
- 默认fair值是false, 它是非公平模式,在非公平模式下,当有很多线程在等待锁时,锁将选择他们中的一个来访问临界区
- 如果fair值是true, 称为公平模式。在公平模式下,当有很多线程在等待锁的时候,锁将选择他们中的等待时间最长的一个线程访问临界区。
同步准则
当编写synchronized 块时,有几个简单的准则
- 使代码块保持尖端。把不随线程变化的预处理和后处理移出synchronized 块
- 不要阻塞, 如InputStream.read()
- 在持有锁的时候,不要对其他对象调用同步方法
8、死锁
过多的同步有可能出现死锁,死锁的出现一般是程序运行的时候才有可能出现
多线程要进行资源的共享,就需要同步,但同步过多,就可能造成死锁。