开始线程之旅
先来了解一下线程和进程:
并发:两个或多个事件在同一时刻发生。(真正意义上的同时发生)
并行:两个或多个事件在同一时间段内发生。(宏观上是同时发生,微观上是有先后顺序,线程交替发生)。
单核处理器的计算机肯定是不能并行处理多个任务的,只能是多个任务在单个cpu上并发运行,由cpu调度。
线程调度:宏观上线程是并行运行的,但是微观上却是串行运行的,即一个线程一个线程去运行,当系统只有一个cpu时,线程会以某种顺序执行多个线程。
进程:是指一个内存中运行中的应用程序,每个进程都有自己独立的一块儿内存空间,进程之间通信不方便。
线程:是指进程中的一个执行任务(控制单元),一个进程可以同时并发运行多个线程(多线程)。
二者区别:
进程有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。线程的堆空间是共享的,栈空间是独立的,线程消耗的资源也比进程少,相互之间可以影响,又称轻型进程或者进程元。
关于线程的操作
线程创建方式:
1、继承于java.lang.Thread类--------------------------(此run方法为覆盖)(继承于Thread类的类叫线程类)。
2、实现Runnable接口 -------------------Thread类实现了Runable接口(此run方法为实现 / 覆盖)(不叫线程类)。
3、用匿名内部类来创建和实现线程 。
第一种方式(继承于Thread类):
class Music extends Thread{ //线程类
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println("听音乐");
}
}
}
public class GameDemo1 {
public static void main(String[] args) {
//创建线程对象
Music music=new Music();
music.start();//启动线程不是靠已经覆盖的run方法,而是依靠线程类的start方法
for(int i=0;i<10;i++){
System.out.println("打游戏");
}
}
}
//结果表明,由于cpu调度,具有不确定性
第二种方式(实现Runnable接口):
class mUsic implements Runnable{ //实现类
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println("听音乐");
}
}
}
public class GameDemo2 {
public static void main(String[] args) {
mUsic music=new mUsic();
//创建线程对象并启动需要线程类的start方法,所以new一个线程类并给其传过去一个实现类对象
new Thread(music).start();
for(int i=0;i<10;i++){
System.out.println("打游戏");
}
}
}
第三种方式(使用匿名内部类):
public class GameDemo3 {
public static void main(String[] args) {
new Thread(new Runnable(){
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println("听音乐");
}
}
}).start();
//方式2: java8推荐使用lambda语法定义匿名内部类
/*new Thread(()->{
for(int i=0;i<10;i++){
System.out.println("听音乐");
}
}).start();
*/
for(int i=0;i<10;i++){
System.out.println("打游戏");
}
}
}
继承方式和实现方式比较:
1)从功能上说,都可以。
2)从简单易用上说,继承方式更简单。
3)继承方法无法共享资源,而实现方式可以(比如:3个人搬运一堆苹果)。
4)继承方式只能继承一个类。实现方式可以实现多个接口,也可以继承其他类。
当:
1)该类还需要继承其他类
2)多个线程需要共享资源
此时必须使用实现方式。其他情况,推荐使用实现方式。
线程并发问题:
当多个线程并发访问某一个共同资源的时候,可能会出现线程安全问题。
Thread.sleep方法仅仅是用来是多线程并发访问同一资源时,问题效果变得明显。
解决方案:
1)同步代码块;
2)同步方法;
3)Java5开始支持,锁机制;
案例:
class Student implements Runnable{
private int total=100;
@Override
public void run() {
for(int i=0;i<100;i++) {
if(total>0) {
//习惯模拟延迟Thread.sleep
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "拿走第" + total--);//获取当前线程类的名字
}
}
}
}
public class multithreadingDemo {
public static void main(String[] args) {
Student a=new Student();
new Thread(a,"A").start();
new Thread(a,"B").start();
new Thread(a,"C").start();
}
}
方式一(同步代码块):
同步代码块(使用synchronized(关键字)修饰的代码块)。
Synchronized(同步监听对象){
需要同步的代码
}
class Student1 implements Runnable{
private int total=100;
@Override
public void run() {
for(int i=0;i<100;i++) {
synchronized (this){--------------------------->这里
if(total>0) {
//习惯模拟延迟Thread.sleep
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "拿走第" + total--);//获取当前线程类的名字
}
}
}
}
}
方式二(同步方法):
同步方法(使用synchronized(关键字)修饰的方法)。
Synchronized Public 返回类型 方法名(参数列表){
需要同步的代码
}
class Student2 implements Runnable{
private int total=100;
@Override
public void run() {
for(int i=0;i<100;i++) {
takeAway();
}
}
synchronized public void takeAway(){------------------>这里
if(total>0) {
//习惯模拟延迟Thread.sleep
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "拿走第" + total--);//获取当前线程类的名字
}
}
}
同步方法同步监听的对象是谁:
若方法为非static方法,此时同步监听对象是:this
若方法为static方法,此时同步监听对象是:当前方法所在类的字节码对象(xxx.class)
方式三(锁机制):
class Student3 implements Runnable{
private int total=100;
private final Lock lock = new ReentrantLock();//Lock是一个接口
@Override
public void run() {
for(int i=0;i<100;i++) {
takeAway();
}
}
public void takeAway(){
lock.lock();
try{
if(total>0) {
//习惯模拟延迟Thread.sleep
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "拿走第" + total--);//获取当前线程类的名字
}
}finally{
lock.unlock();
}
}
}
在object类中,操作线程的方法:
都得使用当前同步监听对象来调用。
void wait(long timeout):让当前线程失去同步监听对象,进入等待,若等待时间一到,或者其他线程来唤醒它,才会再次拥有同步监听对象
void wait():让当前线程失去同步监听对象,进入无限制的等待,除非其他线程唤醒它,不然永远醒不过来
void notify():唤醒在此同步监听对象上等待的一个线程。
void notifyAll():唤醒在此同步监听对象上等待的所有线程。
拓展:
死锁:当多个线程都拿着同步监听对象不放,此时造成的情况就是死锁(效果:程序没有停止,但没有任何反应)。
线程的生命周期
在Thread类中,有一个State的内部类(枚举类)。
Public enum State{
NEW , RUNNABLE , BLOCKED , WAITING , TIMED_WAITING , TERMINATED;
}
NEW :至今尚未启动的线程处于这种状态。 (当一个线程对象被创建,还未调用start方法,仅仅是一个普通的java对象,在堆内存分配空间)。
RUNNABLE:正在 Java 虚拟机中执行的线程处于这种状态。(当一个线程对象调用了start方法,可能处于就绪或者正在运行状态)。
BLOCKED:受阻塞并等待某个监视器锁的线程处于这种状态。(线程遇到某个特殊情况如:同步,等待I/O操作完成等)。
WAITING :无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。(除非使用notify或者notifyAll方法) 。
TIMED_WAITING:等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。(当线程使用了wait( long time)或者Thread.sleep( long time)方法) 。
TERMINATED: 已退出的线程处于这种状态。(当线程执行完毕,或者执行过程中遇到异常。线程一旦死亡,就不能再重新启动。在Thread类中,有方法:isAlive(),用来判断线程是否处于活动状态。)
线程控制操作
联合线程:线程的join方法表示一个线程等待另一个线程完成后才执行。(把当前线程和当前线程所在的线程联合成一个线程,join方法被调用了以后,线程对象处于阻塞状态。)
适用于A线程需要等到B线程执行完毕,再拿到B线程的结果再继续运行A线程。(即A线程需要拿到B线程的执行结果,才能继续往下)。
线程睡眠:使用Thread.sleep(long millis)
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。
后台线程:也称为“守护线程”,其目的是为其他线程提供服务。
JVM的垃圾回收器就是典型的后台线程。
特点:若所有的前台线程都死亡,后台线程自动死亡。
测试线程对象是否为后台线程,使用thread.isDaemon( );
前台线程创建的线程默认是前台线程,并且当且仅当创建线程是后台线程时,新线程才是后台线程。
设置后台线程:thread.setDaemon(true),该方法必须在start方法调用前,否则会出现异常。
线程优先级:每个线程都有优先级,优先级的高低只和线程获得执行机会的次数多少有关,并非优先级高的就一定先执行,还得由cpu的调度。
设置优先级:void setPriority( int newPriority);
获取优先级:int getPriority( );
线程最高为10,最低为1,默认为5。
但是开发不建议随意设置优先级。
线程的礼让:yield方法:表示当前线程对象提示调度器自己愿意让出cpu资源,但是调度器可以自由的忽略该提示。
该方法主要用于调试或测试,它可能有助于因多线程竞争条件下的错误重现现象。
拓展:
线程中过时的方法:
Suspend( ): 挂起线程;
Void resume( ): 取消挂起;
Void stop( ):终止线程;
前两者具有死锁倾向,后者具有固有的不安全性。