标题
程序、进程、线程基本概念
- 程序:一段静态的代码;
- 进程:程序的一次执行过程;进程的产生、存在和消亡的过程称为生命周期;
- 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域;
- 线程:进程可进一步细化为线程,是一个程序的一条执行路径,若一个进程同一时间并行执行多个线程,就是支持多线程的;
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器,线程切换的开销小;
- 一个进程中的多个线程共享相同的内存单元或内存地址空间,它们从同一个堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效;
- 多个线程操作共享的系统资源可能就会带来安全隐患;
- 一个Java应用程序java.exe,至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程;
线程的创建和使用
方式一,继承Thread类
- 创建一个继承于Thread类的子类;
- 重写Thread类的run() 方法,将此线程执行的操作声明在run()中;
- 创建Thread类的子类的对象;
- 通过此对象调用start();
方式二,实现Runnable接口
- 创建一个实现了Runnable接口的类
- 实现类去实现Runnable中的抽象方法:run()
- 创建实现类的对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
- 通过Thread类的对象调用start()
两种方式的比较
- 实现Runnable接口的方式使用更频繁,因为实现的方式没有类的单继承性的局限性,而且实现的方式更适合处理多个线程有共享数据的情况;
- 从源码public class Thread implements Runnable可以看出,Thread类也是Runnable接口的一个实现类;
- 两种方式相同点在于都需要重写run(),将线程要执行的逻辑声明在run()中。
thread类的常用方法
- start():启动当前线程;调用当前线程的run();
- run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中;
- currentThread():静态方法,返回执行当前代码的线程;
- getName():获取当前线程的名字;
- setName():设置当前线程的名字;
- yield():释放当前cpu的执行权;
- join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态;
- stop():已过时。当执行此方法时,强制结束当前线程;
- sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒,在指定的millitime毫秒时间内,当前线程是阻塞状态;
- isAlive():判断当前线程是否存活;
线程的优先级
- MAX_PRIORITY:10,最高优先级;
- MIN _PRIORITY:1,最小优先级;
- NORM_PRIORITY:5,默认优先级;
- getPriority():获取线程的优先级;
- setPriority(int p):设置线程的优先级;
- 高优先级的线程要抢占低优先级线程cpu的执行权,但这只是概率意义的抢占;
- 高优先级的线程高概率的情况下被执行;并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。
示例代码
用继承Thread类的方法创建三个线程,模拟三个售票员出售100张火车票,用实现Runnable接口的方法创建三个线程,模拟三个售票员出售100张飞机票。
public class SaleTicket {
public static void main(String[] args) {
Sale saler1 = new Sale("张三");
Sale saler2 = new Sale("李四");
Sale saler3 = new Sale("王五");
saler1.start();
saler2.start();
saler3.start();
System.out.println("******************************分隔符***************************");
Thread saler4 = new Thread(new SaleNew(),"赵六");
Thread saler5 = new Thread(new SaleNew(),"小七");
Thread saler6 = new Thread(new SaleNew(),"重八");
saler4.start();
saler5.start();
saler6.start();
}
}
class Sale extends Thread{
private static int ticketNum = 100; //总共的票数
public Sale(String name){
this.setName(name); //创建实例时给线程起名
}
@Override
public void run() {
while (true){
if (ticketNum>0){
System.out.println(this.getName()+"------" +"卖出了"+"------"+(ticketNum--)+"------"+"号飞机票");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
break;
}
}
}
}
class SaleNew implements Runnable{
private static int ticketNum = 100;
@Override
public void run() {
while (true){
if (ticketNum>0){
System.out.println(Thread.currentThread().getName()+
"------" +"卖出了"+"------"+(ticketNum--)+"------"+"号火车票");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
break;
}
}
}
}
线程的生命周期
线程的一个完整生命周期包括5种状态:
创建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态;
就绪:处于新建状态的线程调用start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源;
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能;
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态;
死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束;
线程的同步
多线程的安全问题
上述示例代码运行结果部分如下图,可以看出有重票情况,这就表明多线程的存在安全问题。
- 线程安全问题:一个进程的多个线程操作共享数据时,某个线程操作共享数据的一段代码只执行了一部分,另一个线程参与进来执行,导致共享数据的未按照预想的规律变化。
- 解决思路:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
- 以上述模拟售卖火车票、飞机票为例,当张三正在卖第48张火车票,先售出48号票,再执行票号减一的操作,但在张三执行票号减一操作之前,李四执行售票操作,由于此时票号依然为48,所以李四售出了与张三重号的票。
同步代码块
同步代码块的格式为:
synchronized(任一对象){同步的代码块;}
synchronized锁:
- 任意对象都可以作为同步锁,所有对象都自动含有单一的锁(监视器);
- 同步方法的锁:静态方法(类名.class)、非静态方法(this);
- 同步代码块的锁:自己指定,很多时候也是指定为this或类名.class;
- 必须确保使用同一个资源的多个线程共用一把锁,才能保证共享资源的安全;
- 一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎);
- 同步的范围要合理,范围太小,不能锁住涉及安全问题的代码;范围太大,降低效率,体现不了多线程的功效;
示例代码:
class Window1 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while(true){
synchronized (this){//此时的this:唯一的Window1的对象 //方式二:synchronized (dog) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket--);
} else {
break;
}
}
}
}
}
class WindowTest1 {
public static void main(String[] args) {
Window1 w = new Window1();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
如果创建多线程时,采用的事把匿名实现类作为参数传给Thread类,则同步代码块起不到同步作用,因为这时三个线程有三把锁,而不是共有一把锁。
同步方法
同步方法的格式为:
public synchronized void test(){方法体;}
- 同步方法直接让整个方法实现同步;
- 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明;
- 非静态的同步方法,同步监视器是:this静态的同步方法,同步监视器是:当前类本身;
释放锁、死锁
释放锁的操作:
- 当前线程的同步方法、同步代码块执行结束;
- 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块或方法;
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束;
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁;
死锁:
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁;
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续;
- 解决问题:设计专门的算法、尽量减少同步资源定义、尽量避免嵌套同步;
死锁示例:
public class DeadLockTest {
public static void main(String[] args) {
final StringBuffer s1 = new StringBuffer();
final StringBuffer s2 = new StringBuffer();
new Thread() {
public void run() {
synchronized (s1) {
s2.append("A");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2) {
s2.append("B");
System.out.println("s1:"+s1);
System.out.println("s2:"+s2);
}
}
}
}.start();
new Thread() {
public void run() {
synchronized (s2) {
s2.append("C");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1) {
s1.append("D");
System.out.println("s2:"+s2);
System.out.println("s1:"+s1);
}
}
}
}.start();
}
}
Lock
- JDK5.0后,Java可以通过显式定义同步锁对象来实现同步,同步锁使用Lock对象充当,java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具;
- 锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象;
- ReentrantLock 类实现了 Lock接口,它拥有与 synchronized相同的并发性和内存语义,ReentrantLock类比较常用,可以显式加锁、释放锁;
Lock锁示例:
class A{
private final ReentrantLock lock = new ReenTrantLock();
public void m(){
lock.lock(); //手动上锁
try{
//保证线程安全的代码;
}
finally{
lock.unlock(); //手一动解锁
}
}
}
synchronized 与 Lock 的对比:
- Lock是显式锁,需手动开启和关闭锁,synchronized是隐式锁,出了作用域自动释放;
- Lock只有代码块锁,synchronized有代码块锁和方法锁;
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类);
Lock锁示例代码:
public class LockTest {
public static void main(String[] args) {
Count count = new Count(5000.0);
Client client = new Client(count);
Thread client1 = new Thread(client,"client1");
Thread client2 = new Thread(client,"client2");
Thread client3 = new Thread(client,"client3");
client1.start();
client2.start();
client3.start();
}
}
class Count{
private double money;
public Count(double money){
this.money = money;
}
//获取余额
public double checkMoney() {
return money;
}
//存钱操作
public void addMoney(double money){
this.money+=money;
System.out.println(Thread.currentThread().getName()+"本次存入"+money+",账户余款变为:"+this.money);
}
}
class Client implements Runnable{
Count count;
private static int counter;
private final ReentrantLock lock = new ReentrantLock(); //产生一把锁
Client(Count count){
this.count = count;
}
@Override
public void run() {
while (true){
lock.lock(); //手动上锁
try {
if (counter<10){
count.addMoney(1000.0);
System.out.println("这是第次"+(++counter)+"存钱"+",由"+Thread.currentThread().getName()+"存的");
}else{
break;
}
} finally {
lock.unlock(); //手动解锁
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
线程的通信
- wait():此方法让当前线程就进入阻塞状态,并释放同步监视器;
- notify():此方法会唤醒被wait的一个线程,如果有多个线程被wait,就唤醒优先级高的那个。
- notifyAll():此方法会唤醒所有被wait的线程;
注意:
- wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中;
- wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则,会出现IllegalMonitorStateException异常;
- wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中的;
sleep与wait的异同:
- 相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态;
- 不同点1:两个方法声明的位置不同,Thread类中声明sleep() , Object类中声明wait();
- 不同点2:调用的要求不同:sleep()可以在任何需要的场景下调用, wait()必须使用在同步代码块或同步方法中;
- 不同点3:关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁;