文档章节

重新开始学Java——多线程基础

大家都是低调来的
 大家都是低调来的
发布于 10/22 13:56
字数 9184
阅读 41
收藏 2

多线程

进程

主流计算机操作系统都支持同时运行多个任务 , 每个任务通常就是一个程序 , 每个运行中的程序就是一个进程或者多个进程 。

进程的特点

  1. 独立性
    • 进程是系统中独立存在的实体
    • 可以拥有自己独立的资源
    • 拥有自己私有的地址空间(在未经允许的情况下, 用户进程不能其他进程的地址空间)
  2. 动态性
    • 进程是一个正在系统中活动的指令集合
      • 与进程不同的是, 程序是一个静态的指令集合
    • 进程具有自己的生命周期和各种不同的状态
      • 与进程不同的是, 程序没有时间的概念, 也就没有生命周期等概念
  3. 并发性
  • 多个进程可以在单个处理器上并发运行
  • 并发运行的各个进程之间不会相互影响

并行与并发

  1. 并行 : parallel
    • 并行, 指的是多个进程在多个处理器上同时执行
      • 正如同多辆汽车行使在多条马路上或者多个车道上多条马路, 可以与我们的多个 CPU 相对应而多个车道, 则可以与我们的单个 CPU 多个内核相对应
  2. 并发 : concurrency
  • 并发, 指同一时刻只能运行一条指令, 但是多个进程指令快速地轮换执行, 在某个时间段内, 看上去具有多个进程同时执行的效果,也就是说, 并发, 并不是真的是由单个CPU同时运行多个程序

并发策略的实现方式

  1. 共享式的多任务操作策略

    所谓共享式多任务, 也称作协作式多任务 如果一个任务获得了CPU时间片, 除非它愿意放弃, 否则它将永远霸占CPU 所以, 为了保证系统正常运作, 各个任务之间需要协作 某个任务获得CPU时间片后, 使用一段时间后释放CPU, 让其他任务有机会运行

  2. 抢占式的多任务操作策略

    所谓抢占式多任务是指 总控制权在操作系统手中, 直接中断而不事先和被中断程序协商 操作系统会轮流询问每一个任务是否需要使用 CPU , 需要使用的话就让它用 不过在一定时间后, 操作系统会剥夺当前任务的 CPU 使用权 把它排在询问队列的最后, 再去询问下一个任务

线程

线程 ( Thread ) 被称作 轻量级进程 ( Lightweight Process )。线程是比进程更小一级的执行单元,如同进程在操作系统中的地位一样, 线程在进程中是独立的、 并发的执行流。

程序、进程、线程

操作系统、程序、进程、线程的关系如下:

在这里可以看到,一个操作系统中可以有多个应用程序,贰一个应用程序中可以多有个进程,而一个进程中可以有多个线程的存在,当然有时候可能也只有一个。

线程的特点

  1. 一个进程可以有多个线程, 但至少有一个线程
    • 当进程被初始化后, 主线程也就被创建了(一般而言, 一个应用程序仅要求有一个主线程(但可以有多个其它线程))
  2. 线程不能独立存在, 必须属于某个进程
    • 线程可以拥有自己的堆栈、 自己的程序计数器、 自己的局部变量
    • 线程不能再独立拥有系统资源(它将与父进程的其它线程共享该进程内所有的系统资源)
  3. 线程可以独立完成某项任务
    • 也可以跟同一进程的其它线程一起完成某项任务
  4. 线程是独立运行的, 它并不知道同进程内的其它线程的存在
  5. 线程的执行是抢占式的
  6. 一个线程可以创建和撤销另一个线程
    • 同一个进程中的多个线程之间可以并发执行

线程 vs 进程

  1. 进程:
    • 每个进程都有独立的代码和数据空间(内存独立)
    • 进程间的切换会有交大的开销
  2. 线程
    • 可以看成是轻量级的进程
    • 同一个进程中的线程共享代码和数据空间(栈内存独立, 堆内存共享)
    • 每个线程有独立的运行栈(方法调用栈)和程序计数器
    • 线程切换的开销小
  3. 二者的联系
    • 一个程序启动后, 至少有一个进程
    • 一个进程里可以包含多个线程, 但是至少要有一个线程
    • 线程不能脱离进程而独自存在

多线程的优势

  1. 同一个进程的各线程间共享内存非常容易
    • 进程不能共享内存, 因此进程间通信没那么容易
  2. 用多线程实现多任务并发比多进程效率高
    • 系统创建进程需要重新分配系统资源, 因此效率不高
    • 而创建线程不需要重新分配系统资源, 因此效率较高
  3. Java 语言简化了多线程编程
  4. Java 语言内置多线程功能支持, 而不是单纯地作为底层操作系统的调度方式

实现线程的方式

实现线程的方式主要有以下几种方式:

  1. 继承java.lang.Thread类
  2. 实现java.lang.Runnable接口
  3. 实现java.lang.Callable接口
  4. 从线程池中拿到对应的线程

但是具体来说,只有继承java.lang.Thread类和从线程池中获取线程的方式才算拿到线程。后边的两种方式在Java并发编程中会进行详细描述。这里不做过多的探究。

示例如下:

  • 继承Thread类
    	// 这里继承了java.lang.Thread类,那么TestThread这个类就是一个线程类。
    	public class TestThread extends Thread{}
    
  • 实现Runnable接口
    	public class TestThread implements Runable{
    		@Override
    		public void run() {}
    	}
    
    	TestThread tt = new TestThread();
    	Thread t = new Thread(tt);
    	t.start();
    

上述两种实例比较简单,具体请参看下面的具体实例。

  • 实现Callable接口:
    	class CallableThread implements Callable<Integer> {
    
    		@Override
    		public Integer call() throws Exception {
    			System.out.println("通过线程池方式创建的线程Callable方式:" + Thread.currentThread().getName() + " ");
    			return 10086;
    		}
    	}
    
    这里的Callable接口仅仅是实现一下,不做具体的使用,具体使用会在Java并发编程种进行详细描述。

Thread & Runnable

这里分别描述了Threa与Runnable的使用,以及具体的方法、构造(如果有的话)等信息。

java.lang.Runnable

官方声明如下:

public interface Runnable

具体描述如下:

The Runnable interface should be implemented by any class whose instances are intended to be executed by a thread. The class must define a method of no arguments called run.
Runnable 接口应该由任何类实现,其实例打算由线程执行。类必须定义无称为 run 的参数的方法。

那么也就是说,如果实现了一个Runnable接口,那么其实例应该有线程来执行,并且实现Runnable接口的类,必须要实现run方法,以便于调用。

Runnable中的方法

Runnable中有且仅有一个方法:run() 。方法具体的描述如下:

void run()
When an object implementing interface Runnable is used to create a thread, starting the thread causes the object's run method to be called in that separately executing thread.
当实现接口 Runnable 的对象用于创建线程时,启动线程会导致在单独执行的线程中调用对象的运行方法。

这个run方法是保证线程进入到运行状态的方法。(关于线程的状态,放在后边进行描述)

java.lang.Thread

官方声明如下:

public class Thread extends Object implements Runnable

在这里可以看到,Thread类直接继承了Object类,并实现了Runnable接口,那么也就是说Thread类是一个关于线程的顶级的类,实现了Runnable接口,那么意味着是可以运行的。

具体的描述如下:

A thread is a thread of execution in a program. The Java Virtual Machine allows an application to have multiple threads of execution running concurrently.
线程是程序中的执行线程。Java 虚拟机允许应用程序同时运行多个执行线程。

Every thread has a priority. Threads with higher priority are executed in preference to threads with lower priority. 
Each thread may or may not also be marked as a daemon. 
When code running in some thread creates a new Thread object, the new thread has its priority initially set equal to the priority of the creating thread, and is a daemon thread if and only if the creating thread is a daemon
每个线程都有一个优先级。具有较高优先级的线程优先于优先级较低的线程执行。
每个线程也可能标记为守护进程,也可能不标记为守护进程。
当在某些线程中运行的代码创建新的 Thread 对象时,新线程的优先级最初设置为等于创建线程的优先级,并且仅当创建线程是守护进程时,才是守护进程线程。

When a Java Virtual Machine starts up, there is usually a single non-daemon thread (which typically calls the method named main of some designated class). The Java Virtual Machine continues to execute threads until either of the following occurs:
当 Java 虚拟机启动时,通常都会有单个非守护线程(它通常会调用某个指定类的 main 方法)。Java 虚拟机会继续执行线程,直到下列任一情况出现时为止:

The exit method of class Runtime has been called and the security manager has permitted the exit operation to take place.
调用了 Runtime 类的 exit 方法,并且安全管理器允许退出操作发生。

All threads that are not daemon threads have died, either by returning from the call to the run method or by throwing an exception that propagates beyond the run method.
非守护线程的所有线程都已停止运行,无论是通过从对 run 方法的调用中返回,还是通过抛出一个传播到 run 方法之外的异常。

在这里说明几点:

  • 在Java虚拟机中可以运行多个线程,而这种基础也是后续并发编程的基础
  • 线程具有优先级,优先级较高,那么就可以先执行
  • 线程中具有守护线程
  • 如果想要退出线程,那么就有两种方法,调用Runtime的exit方法,或出现异常。
  • 线程退出还有一种情况,那么就是整个JVM终止。
构造方法
方法名 英文描述 中文描述
Thread() Allocates a new Thread object. 分配一个新的线程对象。
Thread​(Runnable target) Allocates a new Thread object. 分配一个新的线程对象。
Thread​(Runnable target, String name) Allocates a new Thread object. 分配一个新的线程对象。
Thread​(String name) Allocates a new Thread object. 分配一个新的线程对象。
Thread​(ThreadGroup group, Runnable target) Allocates a new Thread object. 分配一个新的线程对象。
Thread​(ThreadGroup group, Runnable target, String name) Allocates a new Thread object so that it has target as its run object, has the specified name as its name, and belongs to the thread group referred to by group. 分配一个新的线程对象,使其具有目标作为其运行对象,具有指定的名称作为其名称,并属于该组所引用的线程组。
Thread​(ThreadGroup group, Runnable target, String name, long stackSize) Allocates a new Thread object so that it has target as its run object, has the specified name as its name, and belongs to the thread group referred to by group, and has the specified stack size. 分配一个新的线程对象,使其具有目标作为其运行对象,具有指定的名称作为其名称,并且属于该组引用的线程组,并且具有指定的堆栈大小。
Thread​(ThreadGroup group, Runnable target, String name, long stackSize, boolean inheritThreadLocals) Allocates a new Thread object so that it has target as its run object, has the specified name as its name, belongs to the thread group referred to by group, has the specified stackSize, and inherits initial values for inheritable thread-local variables if inheritThreadLocals is true. 分配一个新的Thread对象,使其具有目标作为其运行对象,具有指定的名称作为其名称,属于该组引用的线程组,具有指定的stackSize,并且如果inheritThreadLocals继承可继承的线程局部变量的初始值是真的。
Thread​(ThreadGroup group, String name) Allocates a new Thread object. 分配一个新的线程对象。

通过以上构造,可以发现有多种途径进而构建一个Thread对象,具体的使用还是要根据情况而定。

线程的常用方法

在这里仅仅放上了对应的例子,具体的描述放在了例子中的注释中。

/**
 * 继承java.lang.Thread 类, 重写run方法。
 */
public class FirstThread extends Thread {
    @Override
    public void run() {
        System.out.println("我是FirstThread中的run方法");
        try {
            // sleep方法通常用来模拟网络延迟等情况
            Thread.sleep( 500 );
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("我睡醒了");
    }
}
public class TestThread1  {

    public static void main(String[] args) {
        // 创建一个线程对象,但是此时并没有启动
        FirstThread first = new FirstThread() ;

        // 从Thread类继承的start方法专门用来启动一个线程
        first.start(); // 线程一经启动就会调用线程对象中的run方法。

        // 一个线程只能启动一次
        // first.start(); //此时会触发 java.lang.IllegalThreadStateException异常

        /**
         * Thread.currentThread() : 用户获取当前线程
         */
        System.out.println( Thread.currentThread() );
    }
}

当运行TestThread1.java这个类的时候,结果输出如下:

Thread[main,5,main]
我是FirstThread中的run方法
我睡醒了

注意,因为是main线程先拿到CPU资源的,所以main先执行,当main线程执行完毕之后,随后才轮到first的线程执行。

因为在Java中单继承的,所以继承Thread类会有一些局限性,所以下面的例子中使用实现Runnable接口的形式来操作。

public class SecondThread implements Runnable {
    @Override
    public void run() {
        System.out.println( "当前线程:" + Thread.currentThread() ); // 当前线程:Thread[我是线程,5,main]
        
        // 说明Thread.currentThread() 获取的并不是SecondThread类型的对象,而是下面的线程的对象
        // 两个地址都是一样的,所以说明上面的说法是对的
        System.out.println( System.identityHashCode( Thread.currentThread() )); // 获取地址 1531448569
    }

    public static void main(String[] args) {
        // 创建一个可以运行的对象
        SecondThread s = new SecondThread() ;
        // s.start(); // 这里没有start方法,因此不能说是一个线程

        
        Thread t = new Thread(s , "我是线程") ;
        System.out.println( System.identityHashCode( t )); // 1531448569
        t.start();
    }
}

那么上边就是创建线程的两种方式。

一些其他的方法

public class ThirdThread  {

    public static void main(String[] args) {
        // 获取当前线程
        Thread t = Thread.currentThread() ;
        System.out.println( t ); // Thread[main,5,main]
        // 获取当前线程的名字
        System.out.println( t.getName()  ); // main
        // 获取线程ID
        System.out.println( t.getId() ); // 1
        // 获取当前线程的状态
        System.out.println( t.getState() ); // RUNNABLE
        // 获取线程的优先级
        System.out.println( t.getPriority() ); // 5
        // 获取线程所在的线程组
        System.out.println( t.getThreadGroup() ); // java.lang.ThreadGroup[name=main,maxpri=10]
        // 判断线程是否是存活的,如果是,返回true
        System.out.println( t.isAlive() ); // true
        // 判断当前线程是否是 守护线程( 精灵线程 ),如果是,返回true
        System.out.println( t.isDaemon() ); // false
        // 判断当前线程是否是 中断 ,如果中断,返回true
        System.out.println( t.isInterrupted() ); // false
    }
}
线程调度
  1. 线程调度

    • 单个CPU, 任意时刻只能执行一条机器指令
    • 每个线程只有获得CPU的使用权才能执行指令;
    • 线程调度就是按照特定的机制给多个线程分配CPU使用权
  2. 调度模式

    • 分时调度(协作)模式
      • 让所有线程都轮流获得CPU的使用权
      • 并且平均分配每个线程所占用的CPU时间片
    • 抢占式调度模式
      • 优先让线程池中优先级高的线程占用CPU
      • 当优先级比较高的线程执行完毕后优先级较低的线程才有机会执行
      • 优先级相同的多个线程, 谁先抢到时间片谁就运行
  3. 实现线程调度的方式

    • 调整各个线程的优先级
      • 优先级高的线程获得较多的运行机会
      • 优先级低的线程获得较少的运行机会
    • 让处于运行状态的线程调用 Thread.sleep() 方法
      • 处于运行状态的线程会转入睡眠状态, 其它线程会获得CPU时间片
    • 让处于运行状态的线程调用 Thread.yield() 方法
      • 处于运行状态的线程让位给同等优先级或优先级更高的线程运行
    • 让处于运行状态的线程调用另一个线程的 join() 方法
      • 当前线程A调用另一个线程B的 join() 方法, 当前线程A转入阻塞状态
      • 直到另一个线程B执行结束, 线程A才恢复运行
  4. 示例如下

    • 优先级测试

      	public class PriorityThread extends Thread {
      		@Override
      		public void run() {
      			for (int i = 0 ; i<10 ; i++ ) {
      				// 获取当前的线程
      				System.out.println(i + " : " + PriorityThread.currentThread());
      			}
      		}
      	}
      
      	public class TestPriority1 {
      		public static void main(String[] args) {
      			PriorityThread pt1 = new PriorityThread() ;
      			System.out.println( pt1 );
      
      			PriorityThread pt2 = new PriorityThread() ;
      			pt2.setPriority( Thread.NORM_PRIORITY + 2 );
      			System.out.println( pt2 );
      
      			PriorityThread pt3 = new PriorityThread() ;
      			pt3.setPriority( Thread.NORM_PRIORITY - 2);
      			System.out.println( pt3 );
      			pt1.start();
      			pt2.start();
      			pt3.start();
      		}
      	}
      

      通过改变优先级,进而改变线程的执行,理论上说,优先级越高,那么线程越先执行。但是通过实际的理论测试,执行的顺序不能保证,因为现在的机器大部分都是多核多线程进行操作。 注意事项:

      	设置的优先级不能超过10
      	可以通过对应的Java代码进行设置:Thread.NORM_PRIORITY 、Thread.MAX_PRIORITY 、 Thread.MIN_PRIORITY。这三个常量分别表示5、10、1 。
      	建议通过Java代码的方式进行操作,通过加减的方式进行操作。
      
    • sleep测试

      	public class SleepThread extends Thread  {
      		private boolean sleep ;
      
      		public SleepThread( String name , boolean sleep) {
      			super(name) ;
      			this.sleep = sleep;
      		}
      
      		@Override
      		public void run() {
      			for (int i = 1 ; i < 11 ; i ++ ){
      				System.out.println("第" + i + "次:" + currentThread() );
      				if( i == 6 && sleep ){
      					try {
      						sleep(100);
      					} catch (InterruptedException e) {
      						e.printStackTrace();
      					}
      				}
      			}
      		}
      	}
      
      	/**
      	 * 修改优先级表示 只要调用sleep方法,与优先级无关
      	 */
      	public class TestSleep {
      		public static void main(String[] args) {
      			SleepThread st1 = new SleepThread( "first" , false ) ;
      			st1.setPriority( Thread.MIN_PRIORITY );
      			System.out.println( st1 );
      			SleepThread st2 = new SleepThread( "second" , true ) ;
      			st2.setPriority( Thread.MAX_PRIORITY );
      			System.out.println( st1 );
      			st2.start();
      			st1.start();
      		}
      	}
      
    • yield测试

      	public class YieldThread extends Thread {
      		private boolean yield ;
      
      		public YieldThread( String name , boolean yield) {
      			super(name ) ;
      			this.yield = yield;
      		}
      
      		@Override
      		public void run() {
      			for (int i = 1 ; i < 11 ; i ++ ){
      				System.out.println("第" + i + "次:" + currentThread() );
      				if ( i == 5 && yield ){
      					yield(); // 当前线程让出CPU,重新回到就绪状态,只有同等优先级的线程才有权力进行抢夺CPU
      				}
      			}
      		}
      	}
      
      	public class TestYield {
      		public static void main(String[] args) {
      			YieldThread yield = new YieldThread( "first" , false ) ;
      			yield.setPriority( Thread.NORM_PRIORITY + 2  ) ;
      			YieldThread yield2 = new YieldThread( "second" , true ) ;
      			yield.start();
      			yield2.start();
      		}
      	}
      

      yield方法与线程的优先级有关,在单核的情况下效果十分明显,在多核的情况下效果不明显。

    • join方法

      	public class JoinThread extends Thread {
      		private Thread another ;
      
      		public JoinThread(String name , Thread another) {
      			super(name) ;
      			this.another = another;
      		}
      
      		@Override
      		public void run() {
      			for (int i = 1 ; i <11 ; i++) {
      				System.out.println( currentThread() + " : " + i);
      				if( i == 5 && Objects.nonNull( another ) ) {
      					try {
      						another.join();
      					} catch (InterruptedException e) {
      						e.printStackTrace();
      					}
      				}
      			}
      		}
      	}
      
      	public class TestJoin {
      		public static void main(String[] args) {
      			JoinThread j = new JoinThread("First" , null ) ;
      			JoinThread j2 = new JoinThread("Second", j );
      			j.start();
      			j2.start();
      		}
      	}
      
    • sleep( ) 与 yield() 方法的共同点

      • 都属于 Thread 类的静态方法
        	可以使用 Thread 调用, 也可以使用线程对象调用
        	不论是 sleep( ) 还是 yield( ) 都必须在线程内部调用
        	在线程外部调用这些方法, 即便是使用某个线程对象调用, 也不会对该线程对象有任何影响, 反而是对该线程所在的线程有影响
        
      • 都会使处于运行状态的线程放弃CPU, 给别的线程机会当前线程会放弃 CPU , 或转入 就绪状态 或转入 阻塞状态
    • sleep( ) 与 yield() 方法的区别

      • 是否考虑优先级
        	sleep() 给其他线程运行的机会, 不考虑其他线程的优先级,因此会给优先级较低的线程一个运行的机会
        	yield() 只会给相同优先级或者更高优先级的线程一个运行的机会比当前线程优先级低的线程没有机会获得 CPU 时间片
        
      • 转入状态不同
        	线程调用 sleep( long millis ) 后转入阻塞状态
        	线程调用 yield() 方法后直接转入就绪状态
        
      • 是否抛出异常
        	sleep() 方法声明抛出 InterruptedException 异常
        	yield() 方法不抛出任何异常
        
      • 可移植性
        	sleep() 较 yield() 有更好的可移植性
        	不能依赖于 yield() 来提高程序的并发性能
        	推荐在测试时使用 yield() , 其他情况能不用就不用
        
synchronized关键字

synchronized 关键字,代表这个方法(或代码块)加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。它包括两种用法:synchronized 方法 和 synchronized 代码块 。

简单来说,synchronized关键字以同步方法和同步代码块的方式,为方法和代码块上的对象加锁。使得同一时刻,在这个对象上的多个线程,只能由持有这个对象锁的单个线程进行代码的调用执行。

  • 用法概述

    	同步代码块
    		加锁对象是本地变量的同步代码块
    		加锁对象是类静态变量的同步代码块
    		加锁对象是共享变量的同步代码块
    		加锁对象是类对象的同步代码块
    	同步方法
    		修饰方法是普通方法的同步方法
    		修饰方法是类静态方法的同步方法
    
  • 同步代码块和同步方法

    	相同点:
    		同步代码块和同步方法都通过synchronized关键字实现
    		同步代码块和同步方法都能够保证代码的同步性
    	不同的:
    		同步代码块比同步方法的锁住的范围更小,所以性能更好。
    		同步方法比同步代码块的编写更简单,只需要在方法定义是加上synchronized关键字即可,而后者还需要确定加锁对象。
    
  • 示例如下

public class Account {
    private String cardNo ; // 卡号
    private String password ; // 密码
    private Double balance ; // 余额
    public Account(String cardNo, String password, double balance) {
        super();
        this.cardNo = cardNo;
        this.password = password;
        this.balance = balance; // 此时是你在办卡的时候存入的钱
    }
    /**
     * draw 取款方法
     * @param  money 表示要取多少钱
     * @return 取出了多少钱
     */
    public double draw( double money ){
        double m = 0 ;
        if( this.balance >= money ){
            System.out.println("取走了"+ money + "钱");
            m = money ; // 把取到的钱装起来
            this.balance = this.balance - money ;
            System.out.println( "此时余额为:" + this.balance + "元");
        }else{
            System.out.println("余额不足");
        }
        return m ;
    }
}
public class DrawThread extends  Thread {
    private Account ac  ;
    private Double money ;
    public DrawThread(String name , Account ac , Double money ) {
        super(name);
        this.ac = ac ;
        this.money = money ;
    }
    @Override
    public void run() {
        // 多个线程竞争的那个资源 就是 同步锁
        // 在这里就是同一个账号对象
        synchronized (ac) {
            System.out.println(Thread.currentThread().getName() + " 取走了 :" + money );
            ac.draw(money);
        }
    }
}

此时我们使用了Java中最长的关键字synchronized,这个关键字用来加同步锁,谁是被竞争的资源,谁就是同步锁。

一般以多条线程共享的资源 (竞争资源) 作为同步锁 同步的基本原理是:加锁 → 同步代码块顺利执行 → 释放锁。

当然,如果想要将同步代码块改成同步方法,也是可以的,实例如下:

public class Account3 {
    private Double balance ; // 余额
    public Account3( double balance) {
        super();
        this.balance = balance; // 此时是你在办卡的时候存入的钱
    }
    /**
     * draw 取款方法为一个同步方法,该方法的方法体就是同步代码块,this就是同步锁
     * @param  money 表示要取多少钱
     * @return 取出了多少钱
     */
    public synchronized double draw( double money ){
        double m = 0 ;
        if( this.balance >= money ){
            System.out.println(Thread.currentThread().getName() + "取走了"+ money + "钱");
            m = money ; // 把取到的钱装起来
            this.balance = this.balance - money ;
            System.out.println( "此时余额为:" + this.balance + "元");
        }else{
            System.out.println("余额不足");
        }
        return m ;
    }
}
public class DrawThread3 extends  Thread {
    private Account3 ac  ;
    private Double money ;
    public DrawThread3(String name , Account3 ac , Double money ) {
        super(name);
        this.ac = ac ;
        this.money = money ;
    }
    @Override
    public void run() {
        ac.draw(money ) ;
    }
}
public class ICBC {
    public static void main(String[] args) {
        Account3 ac3 = new Account3(1000) ;
        DrawThread3 dt1 = new DrawThread3("kaka" , ac3 , 800.0 ) ;
        DrawThread3 dt2 = new DrawThread3("kakaTM" , ac3 , 800.0 ) ;
        dt1.start();
        dt2.start();
    }
}

同步方法是指被 synchronized 关健字修饰的方法 对于同步方法而言,无需显式指定同步锁 同步方法的同步锁是 this ,就是当前对象本身。

  • 同步注意

    可变类的线程安全是以牺牲程序的效率为代价的

  • 最好提供两个版本:

    • 如果可变类有两种运行环境:单线程环境和多线程环境
    • 则应该为该类提供两个版本:单线程版本和多线程版本
  • 线程同步的特点

    • 有同步代码块仍然有可能造成线程安全问题

如果一个同步代码块和非同步代码块同时操纵共享资源,仍然会造成对共享资源的竞争,程序员必须准确把握那些资源必须用同步代码块访问

  • 每个对象都有惟一的同步锁

前述举例中,如果有多个 Account 对象,每个对象都会有其独立的一把锁,在静态方法前面也可以使用 synchronized 关键字,无论对普通方法还是对静态方法,而其同步锁是多个线程竞争的那个资源;对于普通方法,其同步锁即为 this。静态方法的同步锁,需要根据实际代码来确定(本质上还是被竞争的资源)

  • 一个线程开始执行同步代码块时,不一定以不中断的方式运行

当在线程中调用了 sleep() 或者 yield() 都有可能导致线程中断

  • synchronized 修饰的方法是可以被继承的

  • 释放同步锁的锁定

    • 当前线程的同步代码块、同步方法执行完毕
    • 当前线程的同步代码块、同步方法遇到 break 、return 语句
    • 当前线程的同步代码块、同步方法异常结束
    • 当前线程执行同步代码块、同步方法时,程序执行了同步锁的wait () 方法,当前线程暂停,并释放同步锁
  • 不释放同步锁的情况

    • 线程执行同步代码块、同步方法时,调用 Thread.sleep() 或Thread.yield() 来暂停当前线程,当前线程不会释放同步锁
    • 线程执行同步代码块时,其他线程调用了该线程的 suspend 方法将该线程挂起,该线程不会释放同步锁
    • 注意:suspend已经被废除了
Object中的wait、notify、notifyAll
  • wait方法:
    	wait():wait()是Object的方法。
    	wait():让当前线程等待,直到另一个线程调用了当前对象上的notify()或者notifyAll()方法。
    	wait():在调用时,会释放当前对象的监视器的所有权(可以理解成解锁)。
    	调用wait()类的线程必须拥有这个对象的监视器(可以理解成锁)。
    	wait()的线程会一直等待,直到另一线程通知当前对象上的所有线程通过notify()唤醒单个线程或者通过notifyAll()唤醒全部线程。
    	wait()也可以只等待一定的时间就自动唤醒,方法是wait(long)。
    
  • notify方法:
    	notify():唤醒等待对象监视器的单个线程。
    	notify():如果等待对象监视器的有多个线程,则选取其中一个线程进行唤醒。
    	notify():选择唤醒哪个线程是任意的,由CPU自己决定。
    
  • notifyAll方法 notifyAll():唤醒等待对象监视器的所有线程。

很显然,notify和notifyAll一个是唤醒单个线程,一个是唤醒所有线程,前提是都在指定的对象监视器上。

示例如下:

public class Account {
    /**
     * 账户余额
     */
    private Double balance;
    /**
     * 标识账户上是否有钱:有-账户上有钱,可以取,无-账户上没钱,需要存
     */
    private char flag = '无';
    public Account(Double balance) {
        if (balance > 0) {
            this.balance = balance;
            this.flag = '有'; // 如果在开户时存钱了,那么就说明有钱
        }
    }
    // 专门用来判断有钱没钱的一个 同步方法
    public synchronized boolean has(){
        return this.flag == '有' ;
    }
    /**
     * 一个专门用来从当前账号上进行取钱的同步方法( 其同步锁时当前对象 )
     * @param money
     * @return
     */
    public synchronized double draw(double money) {
        double m = 0 ;
        if (this.flag == '无') {
            // 如果没有钱,那么就要等待,让别人给你存钱
                // 调用同步锁的wait方法,谁调用谁去等待( 进入到等待池 )
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            Thread th = Thread.currentThread() ;
            System.out.println(th.getName() +" 准备取【" + money +"】元");
            this.balance = this.balance - money;
            m = money ;
            this.flag='无' ;
            System.out.println(th.getName() + "的账户余额为【" + this.balance + "】元");
            this.notifyAll(); // 唤醒正在等待当前同步锁的所有线程
        }
        return m ;
    }
    public synchronized  void deposit( double money ){
        if( this.flag == '有'){
            // 如果账户上有钱,谁调用谁去等待( 进入到等待池 )
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            Thread th = Thread.currentThread() ;
            System.out.println(th.getName() +" 准备存【" + money +"】元");
            this.balance = this.balance + money;
            this.flag = '有' ;
            System.out.println(th.getName() + "的账户余额为【" + this.balance + "】元");
            this.notifyAll(); // 唤醒正在等待当前同步锁的所有线程
        }
    }
}
public class DepositThread extends  Thread {
    private Account ac  ;
    private double money ;

    public DepositThread(Account ac, double money) {
        this.ac = ac;
        this.money = money;
    }
    @Override
    public void run() {
        for( int i = 1 ; i <= 3 ; i++ ){
            if( ac.has() ) {
                i-- ;
            } else {
                ac.deposit( money );
            }
        }
    }
}
/**
 * 取钱的线程,要取30次(消费者)
 */
public class DrawThread extends Thread {
    private Account ac ;
    private double money ;
    public DrawThread(Account ac , double money ) {
        this.money = money ;
        this.ac = ac;
    }
    @Override
    public void run() {
        for( int i = 1 ; i <= 10 ; i++ ){
            if( ac.has() ) {
                ac.draw( money ) ;
            } else {
                i-- ;
            }
        }
    }
}
public class Test {
    public static void main(String[] args) {
        final Account a = new Account( 1000.0 );
        DrawThread dt = new DrawThread( a , 1000 );
        DepositThread de1 = new DepositThread( a ,  1000 );
        DepositThread de2 = new DepositThread( a ,  1000 );
        DepositThread de3 = new DepositThread( a ,  1000 );
        dt.start();
        de1.start();
        de2.start();
        de3.start();
    }
}
线程的状态

线程有多种状态,当然也有其对应表示状态的类(Thread.State),那么在这里就详细说明一下线程的状态。

Thread.State中有几个值,如下所示:

  • NEW:一个尚未启动的线程的状态。也称之为初始状态、开始状态。
  • RUNNABLE:一个可以运行的线程的状态,可以运行是指这个线程已经在JVM中运行了,但是有可能正在等待其他的系统资源。也称之为就绪状态、可运行状态。
  • BLOCKED:一个线程因为等待监视锁而被阻塞的状态。也称之为阻塞状态。
  • WAITING:一个正在等待的线程的状态。也称之为等待状态。造成线程等待的原因有三种,分别是调用Object.wait()、join()以及LockSupport.park()方法。处于等待状态的线程,正在等待其他线程去执行一个特定的操作。例如:因为wait()而等待的线程正在等待另一个线程去调用notify()或notifyAll();一个因为join()而等待的线程正在等待另一个线程结束。
  • TIMED_WAITING:一个在限定时间内等待的线程的状态。也称之为限时等待状态。造成线程限时等待状态的原因有五种,分别是:Thread.sleep(long)、Object.wait(long)、join(long)、LockSupport.parkNanos(obj,long)和LockSupport.parkUntil(obj,long)。
  • TERMINATED:一个完全运行完成的线程的状态。也称之为终止状态、结束状态。

那么具体的转换如图所示:

具体示例如下
  • TIME_WAITING的状态转换

    如果想要达到这种状态,那么就有以下的线程状态的改变:NEW->RUNNABLE->TIME_WAITING->RUNNABLE->TERMINATED。示例如下:

    	public class Time_Waiting {
    		public static void main(String[] args) throws Exception{
    			//线程状态间的状态转换:NEW->RUNNABLE->TIME_WAITING->RUNNABLE->TERMINATED
    			System.out.println("======线程状态间的状态转换NEW->RUNNABLE->TIME_WAITING->RUNNABLE->TERMINATED======");
    			//定义一个内部线程
    			Thread thread = new Thread(() -> {
    				System.out.println("2.执行thread.start()之后,线程的状态:" + Thread.currentThread().getState());
    				try {
    					//休眠100毫秒
    					Thread.sleep(100);
    				} catch (InterruptedException e) {
    					e.printStackTrace();
    				}
    				System.out.println("4.执行Thread.sleep(long)完成之后,线程的状态:" + Thread.currentThread().getState());
    			});
    			//获取start()之前的状态
    			System.out.println("1.通过new初始化一个线程,但是还没有start()之前,线程的状态:" + thread.getState());
    			//启动线程
    			thread.start();
    			//休眠50毫秒
    			Thread.sleep(50);
    			//因为thread1需要休眠100毫秒,所以在第50毫秒,thread1处于sleep状态
    			System.out.println("3.执行Thread.sleep(long)时,线程的状态:" + thread.getState());
    			//thread1和main线程主动休眠150毫秒,所以在第150毫秒,thread1早已执行完毕
    			Thread.sleep(100);
    			System.out.println("5.线程执行完毕之后,线程的状态:" + thread.getState() + "\n");
    
    		}
    	}
    
  • WAITING的状态转换

    如果想要达到这种状态,那么就有以下的线程状态的改变:NEW->RUNNABLE->WAITING->RUNNABLE->TERMINATED。示例如下:

    	public class Waiting {
    		public static void main(String[] args) throws Exception{
    			//线程状态间的状态转换:NEW->RUNNABLE->WAITING->RUNNABLE->TERMINATED
    			System.out.println("======线程状态间的状态转换NEW->RUNNABLE->WAITING->RUNNABLE->TERMINATED======");
    			//定义一个对象,用来加锁和解锁
    			Object obj = new Object();
    			//定义一个内部线程
    			Thread thread1 = new Thread(() -> {
    				System.out.println("2.执行thread.start()之后,线程的状态:" + Thread.currentThread().getState());
    				synchronized (obj) {
    					try {
    						//thread1需要休眠100毫秒
    						Thread.sleep(100);
    						//thread1100毫秒之后,通过wait()方法释放obj对象是锁
    						obj.wait();
    					} catch (InterruptedException e) {
    						e.printStackTrace();
    					}
    				}
    				System.out.println("4.被object.notify()方法唤醒之后,线程的状态:" + Thread.currentThread().getState());
    			});
    			//获取start()之前的状态
    			System.out.println("1.通过new初始化一个线程,但是还没有start()之前,线程的状态:" + thread1.getState());
    			//启动线程
    			thread1.start();
    			//main线程休眠150毫秒
    			Thread.sleep(150);
    			//因为thread1在第100毫秒进入wait等待状态,所以第150秒肯定可以获取其状态
    			System.out.println("3.执行object.wait()时,线程的状态:" + thread1.getState());
    			//声明另一个线程进行解锁
    			new Thread(() -> {
    				synchronized (obj) {
    					//唤醒等待的线程
    					obj.notify();
    				}
    			}).start();
    			//main线程休眠10毫秒等待thread1线程能够苏醒
    			Thread.sleep(10);
    			//获取thread1运行结束之后的状态
    			System.out.println("5.线程执行完毕之后,线程的状态:" + thread1.getState() + "\n");
    		}
    	}
    
  • BLOCKED的状态转换

    如果想要达到这种状态,那么就有以下的线程状态的改变:NEW->RUNNABLE->BLOCKED->RUNNABLE->TERMINATED。示例如下:

    	public class Blocked {
    		public static void main(String[] args) throws Exception{
    			//线程状态间的状态转换:NEW->RUNNABLE->BLOCKED->RUNNABLE->TERMINATED
    			System.out.println("======线程状态间的状态转换NEW->RUNNABLE->BLOCKED->RUNNABLE->TERMINATED======");
    			//定义一个对象,用来加锁和解锁
    			Object obj2 = new Object();
    			//定义一个线程,先抢占了obj2对象的锁
    			new Thread(() -> {
    				synchronized (obj2) {
    					try {
    						//第一个线程要持有锁100毫秒
    						Thread.sleep(100);
    						//然后通过wait()方法进行等待状态,并释放obj2的对象锁
    						obj2.wait();
    					} catch (InterruptedException e) {
    						e.printStackTrace();
    					}
    				}
    			}).start();
    			//定义目标线程,获取等待获取obj2的锁
    			Thread thread3 = new Thread(() -> {
    				System.out.println("2.执行thread.start()之后,线程的状态:" + Thread.currentThread().getState());
    				synchronized (obj2) {
    					try {
    						//thread3要持有对象锁100毫秒
    						Thread.sleep(100);
    						//然后通过notify()方法唤醒所有在ojb2上等待的线程继续执行后续操作
    						obj2.notify();
    					} catch (InterruptedException e) {
    						e.printStackTrace();
    					}
    				}
    				System.out.println("4.阻塞结束后,线程的状态:" + Thread.currentThread().getState());
    			});
    			//获取start()之前的状态
    			System.out.println("1.通过new初始化一个线程,但是还没有thread.start()之前,线程的状态:" + thread3.getState());
    			//启动线程
    			thread3.start();
    			//先等100毫秒
    			Thread.sleep(50);
    			//第一个线程释放锁至少需要100毫秒,所以在第50毫秒时,thread3正在因等待obj的对象锁而阻塞
    			System.out.println("3.因为等待锁而阻塞时,线程的状态:" + thread3.getState());
    			//再等300毫秒
    			Thread.sleep(300);
    			//两个线程的执行时间加上之前等待的50毫秒以供250毫秒,所以第300毫秒,所有的线程都已经执行完毕
    			System.out.println("5.线程执行完毕之后,线程的状态:" + thread3.getState());
    
    		}
    	}
    

守护线程

守护线程也称之为后台线程、精灵线程 当有非守护线程运行时,守护线程一定在运行 所有的非守护线程都退出后,守护线程一定退出。示例如下:

public class DaemonThread extends  Thread {
    @Override
    public void run() {
        while ( true ){
            System.out.println("精灵线程");
        }
    }
}
public class TestDaemonThread {
    public static void main(String[] args) throws InterruptedException {
        Thread main = Thread.currentThread() ;
        System.out.println( main );
        // 是否是精灵线程
        System.out.println( main.isDaemon() );
        DaemonThread dt = new DaemonThread() ;
        // 是否为精灵线程
        System.out.println( main.isDaemon() );
        // 设置为精灵线程
        dt.setDaemon( true );
        System.out.println( dt );
        dt.start(); // 一定要在启动线程之前,就设置它是精灵线程
        Thread.sleep(1000);
    }
}

定时器

定时器用于定时执行特定的任务 通过 java.util.Timer 类的对象可以定时执行某项特定任务。但是在实际应用中,更多的是采用框架内部的定时器。

public class TestTimer1 {
	public static void main(String[] args) {
			// 创建一个计时器
		Timer t = new Timer();
		TimerTask task = new TimerTask() {
			@Override
			public void run() {
				System.out.println( new Timestamp( System.currentTimeMillis() ) );
			}
		};
	
		System.out.println( new Timestamp( System.currentTimeMillis() ) );
		//t.schedule( task , 3000 ); // 从 "现在" 起延迟 5000 毫秒后执行 task (定时执行一次)
		
		// 从现在起 1000 毫秒后 第一次执行 task ,然后 每隔100毫秒再执行一次(周期性执行)
		t.schedule( task , 1000 , 100 );
		
		//t.cancel(); // 取消定时器
	}

}
public class TestTimer2 {

	public static void main(String[] args) {
		
		// 创建一个计时器
		Timer t = new Timer();
		
		TimerTask task = new TimerTask() {
			@Override
			public void run() {
				System.out.println( new Timestamp( System.currentTimeMillis() ) );
			}
		};
		
		final Calendar c = Calendar.getInstance();
		
		c.set( 2016 , 8 , 14 , 9 , 33 , 20 );
		c.set( Calendar.MILLISECOND , 0 );
		
		Date executeTime = c.getTime();
		
		t.schedule( task , executeTime , 1000 );
		
		
	}

}

结束语

这里描述了多线程的基本操作,关于多线程的并发操作,放在以后进行描述。如果有这里有什么错误,请评论指正。

© 著作权归作者所有

大家都是低调来的
粉丝 6
博文 36
码字总数 103285
作品 0
西安
程序员
私信 提问
JAVA基础再回首(三十)——JAVA基础再回首完美结束,感概万千!

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/m366917/article/details/52724939 JAVA基础再回首(三十)——JAVA基础再回首完美结束,感概万千! 经过了几...

Aduroidpc
2016/10/02
0
0
零基础学Java10系列三:Java高级编程​​​​​​​

更多关于Java高级编程内容链接:零基础学Java10系列三:Java高级编程——阿里云大学 多线程: 生产者和消费者指的是两个不同的线程类对象,两个进程公用一个公共的固定大小缓冲区。其中之一的...

阿里云大学云百科
04/26
42
0
Java程序员从笨鸟到菜鸟全部博客目录【2012年十一月七日更新】

本文来自:曹胜欢博客专栏。转载请注明出处:http://blog.csdn.net/csh624366188 大学上了一年半,接触java也一年半了,虽然中间也有其他东西的学习,但是还是以java为主路线,想想这一年半,...

长平狐
2012/11/12
233
0
《Java程序员由笨鸟到菜鸟》电子版书正式发布,欢迎大家下载

在众多朋友的支持和鼓励下,《Java程序员由菜鸟到笨鸟》电子版终于和大家见面了。本电子书涵盖了从java基础到javaweb开放框架的大部分内容。在编写的过程中,难免会出现一些错误,希望大家能...

长平狐
2012/11/12
274
0
《Java程序员由笨鸟到菜鸟》电子版书正式发布,欢迎大家下载

在众多朋友的支持和鼓励下,《Java程序员由菜鸟到笨鸟》电子版终于和大家见面了。本电子书涵盖了从java基础到javaweb开放框架的大部分内容。在编写的过程中,难免会出现一些错误,希望大家能...

长平狐
2012/11/12
163
0

没有更多内容

加载失败,请刷新页面

加载更多

nettysocetio-demo1(nettysocetio通讯,两客户端聊天,群发消息改造)

前言: 网上大多数都是只能群发,或者只能发给自己.并没有一个案例完整的群发并且又可以客户端之间聊天的案例,特此改造好的案例给大家分享一下.只要是一对一聊天,一对多群发. 内容: 废话不多说...

RobertZhou
37分钟前
3
0
在Serverless Kubernetes集群中轻松运行Argo Workflow

导读 Argo是一个基于kubernetes实现的一个Workflow(工作流)开源工具,基于kubernetes的调度能力实现了工作流的控制和任务的运行。 目前阿里云容器服务ACK集群中已经支持工作流的部署和调度,...

阿里云官方博客
39分钟前
3
0
后端的轮子(三)--- 缓存

前言 前面花了一篇文章说数据库这个轮子,其实说得还很浅很浅的,真正的数据库比这复杂不少,今天我们继续轮子系列,今天说说缓存系统吧。 缓存是后端使用得最多的东西了,因为性能是后端开发...

java后端开发
47分钟前
3
0
​京交会组委会企业回访 信必优将携新产品再出发

2020年京交会将于明年5月28日至6月1日在北京举办。为给各界客商提供更多潜在合作机会,打造“永不落幕京交会”,11月12日,京交会组委会办公室举办首场会后集中采访活动,对入选“2019年京交...

symbiochina88
49分钟前
3
0
读「SOLID」的设计原则记录

阅读链接:https://xueyuanjun.com/post/9719 单一职责原则(Single Responsibility Principle) 一个类只做某一件事。 例:操作订单时我们需要查询数据进行验证 如果在订单类中直接查询MyS...

子尤-
53分钟前
3
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部