进程与线程
进程
进程(作为资源分配的单位):进程是指运行中的应用程序,每个进程都有自己独立的地址空间(内存空间),比如用户点击桌面的IE浏览器,就启动了一个进程,操作系统就会为该进程分配独立的地址空间。当用户再次点击左面的IE浏览器,又启动了一个进程,操作系统将为新的进程分配新的独立的地址空间。目前操作系统都支持多进程。(要点:用户每启动一个进程,操作系统就会为该进程分配一个独立的内存空间。)
线程
线程(调度和执行的基本单位)(一个进程可以包含多个线程):是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。线程有就绪、阻塞和运行三种基本状态。
线程的创建
继承Thread类
public class Test extends Thread{ public static void main(String[] args){ Test th = new Thread; th.start(); } @Override void run{ System.out.println("我是一个子线程"); } }
实现Runnable接口
//创建线程 public class TestThread02 implements Runnable { @Override public void run() { for (int i=0; i<20; i++){ System.out.println("1"); } } } //启动线程 public class StartThread { public static void main(String[] args) { TestThread02 thread02 = new TestThread02(); Thread t = new Thread(thread02); t.start(); for (int i=0; i<20; i++){ System.out.println("0"); } } }
实现Callable接口
Callable接口时Runnable接口的增强版,可以抛出异常,可以返回值。其线程主体执行的方法不在时run方法,而是call方法。实现了Callable接口的线程的执行需要借助于FutureTask实现类的支持。并且其可以接受返回值。
public class TestCallable { public static void main(String[] args) throws ExecutionException, InterruptedException { CallableDemo callableDemo = new CallableDemo(); FutureTask<Integer> integerFutureTask = new FutureTask<>(callableDemo); new Thread(integerFutureTask).start(); Integer sum = integerFutureTask.get(); System.out.println("计算结果为:" + sum); } } class CallableDemo implements Callable<Integer> { @Override public Integer call() throws Exception { int sum = 0; for (int i = 0; i <= 100; i++) { sum += i; } return sum; } }
Thread类和Runnable接口有什么区别:
Thread类不适用多线程资源共享。Runnable接口容易实现多线程资源共享。
public class TestThread { public static void main(String[] args) { MyThread1 mt1 = new MyThread1(); MyThread1 mt2 = new MyThread1(); MyThread1 mt3 = new MyThread1(); mt1.start(); mt2.start(); mt3.start(); MyThread2 myThread2 = new MyThread2(); Thread t1 = new Thread(myThread2, "t1"); Thread t2 = new Thread(myThread2, "t2"); Thread t3 = new Thread(myThread2, "t3"); t1.start(); t2.start(); t3.start(); } } class MyThread1 extends Thread{ private int ticket = 5; @Override public void run() { for (int i = 0; i < 100; i++){ if (ticket > 0){ System.out.println(Thread.currentThread().getName()+"卖票:ticket" + ticket --); } } } } class MyThread2 implements Runnable{ private int ticket = 10; @Override public void run() { for (int i = 0; i<100; i++){ if (ticket > 0){ System.out.println(Thread.currentThread().getName()+"卖票:ticket" + ticket -- ); } } } }
继承Thread类,就是多个线程各自完成自己的任务;实现Runnable接口,就是多个线程共同完成一个任务。可见,实现Runnable接口相对于继承Thread类来说,有如下优势:1.适合相同程序的多个线程去处理同一资源的情况。2.可以避免由于Java的单继承特性带来的局限。3.增强了程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。
线程的相关方法
start()方法
开启线程,告知调度器此线程可以被调用,不一定能立即执行,需要被调度才能执行。
run()方法
线程体,直接调用run()方法相当于是调用方法,而不是创建线程。
sleep()方法
线程睡眠,睡眠时间以毫秒为单位,线程会放弃cpu,但是不会释放所持有的锁,并将运行状态的线程转到阻塞状态,当睡眠时间结束后,转到就绪状态等待cpu调度执行。
yield()方法
线程让步。当某个线程在运行中执行了yield()方法,如果此时具有相同优先级别以及更高优先级别的线程处于就绪状态,那么yield()方法就会把当前运行的线程放入可运行池中并使另一个线程运行。如果没有相同或更高优先级别的线程处于就绪状态,那么yield()方法就什么都不做。同sleep()方法,yield()方***放弃cpu,但是是不会释放所持有的锁。
- sleep()方***给其他线程运行的机会,而不考虑其他线程的优先级,所以低优先级的线程也有得到运行的机会。而yield()方法只会让同优先级或更高优先级的线程运行。
- sleep()方法将运行中的线程转到阻塞状态,睡眠时间结束转到就绪状态,但yield()方法是将运行中的线程直接转到就绪状态。
- yield()方法在实际开发中不推荐使用。yield()方法的主要用途是在测试中。
join()方法
等待其他线程结束。当前运行的线程可以调用另一个线程的join()方法,当前线程将会转到阻塞状态,直到另一个线程运行结束,它才会恢复执行(阻塞状态转到运行状态)。
- 两种重载
- public void join()
- public void join(long timeout):其中的timeout为当前线程被阻塞的时间,超过时间后当前线程会恢复运行。
wait()方法
执行该方法的线程释放对象的锁,java虚拟机并将该线程放到该对象的等待池中(处于阻塞状态),该线程等待其他线程将它唤醒。
- wait()方法必须放在循环中。
notify()方法
执行该方法的线程唤醒在对象的等待池中正处于等待的一个线程。java虚拟机将会从对象的等待池中随机选择一个线程,并将其转到对象的锁池中。
notifyAll()方法
该方***把对象等待池中的所有线程都转到对象的锁池中。
interrupt()方法
中断阻塞。例如一个线程处于阻塞状态,调用这个线程的interrupt()方法,被阻塞的线程就会接收到一个InterruptException,并退出阻塞状态,开始进行异常处理。
线程的状态
新生态
线程对象一旦创建(Thread t = new Thread();)就进入新生态(当进入新生态后,每个线程就有了自己的内存空间(堆区))。
就绪态
当调用了start()方法则进入就绪状态,Java虚拟机会为它创建方法调用栈和程序计数器,但不是意味着立即调度执行,而是等待获得cpu的使用权。
- start()方法。
- 解除阻塞事件。
- yield()方法:让当前执行的线程暂停,不是阻塞线程,而是将线程转入为就绪状态。调用了yield()方法后若没有其它等待执行的线程,此时当前线程就会马上恢复执行。
- jvm自动切换到其他线程。
运行状态
当就绪状态的线程获得cpu的调度开始运行,则进入运行状态,此时才真正开始执行线程体的代码块。
阻塞状态
值线程因为某些原因放弃cpu,暂时停止运行。当调用sleep(), wait()或同步锁定时,线程进入阻塞状态,所谓阻塞就是代码不继续往下执行,在等待着,同理不保证调用以上方法就立即阻塞。阻塞事件解除后重新进入就绪状态,等待cpu调度执行才进入运行状态。
- sleep():使线程停止运行一段时间,将处于阻塞状态,时间到达后进入就绪状态。
- wait():
- join():阻塞指定线程等到另一个线程执行完成后在继续执行。
- IO中的read()和write()
- 同步锁定
死亡状态
使线程体的代码执行完毕或者中断执行。一旦进入死亡状态,不能再度调用start()方法再次启动线程。
- stop()不推荐使用。
- destory()不推荐使用。
转换图如下:
线程的优先级
优先级的设定建议在start()方法之前,注意:优先级低只是意味着获得调度的概率低,并不是绝对先调用优先级高后调用优先级低的线程。优先级的范围为(MIN_PRIORITY=1到MAX_PRIORITY=10,默认为NORM_PRIORITY=5)
线程的分类
守护线程(后台线程)
为用户线程服务,JVM不用等待守护线程执行完毕,例如:后台记录操作日志,监控内存的使用。- 后台线程和前台线程相伴相随,只有所有的前台线程都结束生命周期后,后台线程才会结束生命周期。
- 但是前台线程全部结束生命周期后,后台线程不一定会结束生命周期,这个还取决于后台相乘的程序的具体实现。
- 只有在线程调用start()方法之前将其设置为后台线程,如果线程启动后再调用setDaemon()方法,则会抛出IllegalThreadStateException。
- 由前台线程创建的线程在默认情况下为前台线程,由后台线程创建的线程在默认情况下为后台线程。
用户线程
指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。JVM必须确保用户线程执行完毕。
定时器Timer
Timer类可以定时执行特定的任务。TimerTask类表示定时执行的一项任务。
Timer类下的schedule(TimerTask task, long delay, long period)方法用来设置定时器需要定时执行的任务。task表示定时执行的具体任务,delay表示定时器将会在多少毫秒后开始执行,period表示每次执行task任务的时间间隔。
public class TestThread extends Thread { private int a; private static int count; @Override public void start(){ super.start(); Timer timer = new Timer(true);//把与Timer关联的线程设置为后台线程 TimerTask task = new TimerTask() { @Override public void run() { while(true){ reset(); try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }; timer.schedule(task, 10, 500); } @Override public void run() { while(true){ System.out.println(getName()+"-->"+a); if (count++==10) break; yield(); } } private void reset(){ a = 0; } public static void main(String[] args) { TestThread testThread = new TestThread(); testThread.start(); } }
线程安全的类
- 这个类的对象可以同时被多个线程安全的访问。
- 每个线程都能正常执行原子操作,得到正确的结果。
- 在每个线程的原子操作都完成后,对象处于逻辑上合理的状态。
线程同步:线程队列和锁机制(synchronized)
- 一个线程持有锁会导致其他所有需要此锁的线程挂起。
- 在多线程竞争下,加锁和释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
- 如果一个优先级高的线程在等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。
释放对象的锁
- 执行完同步代码块后,会释放锁。
- 在执行同步代码的过程中,遇到异常而导致线程终止,锁也会被释放。
- 在执行同步代码的过程中,执行了锁对象的wait()方法,这个线程会释放锁,进入对象的等待池。
不释放对象的锁
- 在执行同步代码的过程中,执行了Sleep()方法,该方法只会放弃CPU,但不释放锁。
- 在执行同步代码的过程中,执行了yield()方法,该方法只会放弃CPU,但不会释放锁。
- 在执行同步代码的过程中,其他线程执行了当前线程对象的suspend()方法,当前线程被暂停,但不释放锁。
死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,而导致两个或多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有“两个以上对象的锁”时,就有可能发生“死锁”的问题。所以说,产生死锁的必要条件为:
- 资源互斥:一个资源每次只能被一个线程所使用。
- 请求与保持条件:一个线程因请求资源而被阻塞时对已获取的资源保持不放。
- 不剥夺条件:线程已获取的资源在未使用完的情况下不可被剥夺。
- 循环等待条件:多个线程之间行成头尾相接的循环等待资源的关系。
避免死锁
1.粗锁法:使用粒度较粗的锁来消除“请求与保持条件”,缺点是会明显降低程序的并发性能并且会导致资源的浪费。
2.锁排序法:(指定锁获取的顺序)当多个线程都要访问共享资源A,B和C时,保证使每个线程都按照相同的顺序去访问它们,比如持有A的锁才能请求B资源的锁,持有AB资源的锁才可以请求C资源。
线程饥饿
线程饥饿是指线程一直无法获得其所需的资源导致任务一直无法运行的情况。线程调度模式有公平调度和非公平调度两种模式。在线程的非公平调度模式下,就可能出现线程饥饿的情况。
线程组
Java中的ThreadGroup类(不推荐使用)表示线程组,能够对一组线程进行集中管理。
- 可以在创建线程对象通过Thread(ThreadGroup group, String name)构造方法指定线程所属于某个线程组。
- 一旦加入某个线程组,该线程就一直存在于某个线程组中,直至死亡,不能中途改变线程所属的线程组。