一:基本概念:程序、进程、线程
- 程序可以理解为静态的代码
- 进程可以理解为执行中的程序
- 线程可以理解为进程的进一步细分,程序的一条执行路径
使用多线程的优点:
提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
提高计算机系统CPU的利用率
改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
二:线程的创建和使用
Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread
类来体现。
方式一:继承于Thread类
- 1.创建一个继承于Thread类的子类
- 2.重写Thread类的run() --> 将此线程执行的操作声明在run()中
- 3.创建Thread类的子类的对象
- 4.通过此对象调用start()
例子:遍历100以内的所有的偶数
//1. 创建一个继承于Thread类的子类
class MyThread extends Thread {
//2. 重写Thread类的run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
//3. 创建Thread类的子类的对象
MyThread t1 = new MyThread();
//4.通过此对象调用start():①启动当前线程 ② 调用当前线程的run()
t1.start();
//问题一:我们不能通过直接调用run()的方式启动线程。
// t1.run();
//问题二:再启动一个线程,遍历100以内的偶数。不可以还让已经start()的线程去执行。会报IllegalThreadStateException
// t1.start();
//我们需要重新创建一个线程的对象
MyThread t2 = new MyThread();
t2.start();
}
}
// 通过继承Thread类来创建线程类
public class MyThreadTest extends Thread {
private int i;
// 重写run方法,run方法的方法体就是线程执行体
public void run() {
for (; i < 100; i++) {
// 当线程类继承Thread类时,直接使用this即可获取当前线程
// Thread对象的getName()返回当前该线程的名字
// 因此可以直接调用getName()方法返回当前线程的名
System.out.println(getName() + "" + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
// 调用Thread的currentThread方法获取当前线程
System.out.println(Thread.currentThread().getName() + "" + i);
if (i == 20) {
// 创建、并启动第一条线程
new MyThreadTest().start();
// 创建、并启动第二条线程
new MyThreadTest().start();
}
}
}
}
虽然上面程序只显式地创建并启动了2个线程,但实际上程序有3个线程,即程序显式创建的2个子线程和1个主线程。前面已经提到,当Java程序开始运行后,程序至少会创建一个主线程,主线程的线程执行体不是由run()方法确定的,而是由main()方法确定的,main()方法的方法体代表主线程的线程执行体。
该程序无论被执行多少次输出的记录数是一定的,一共是300条记录。主线程会执行for循环打印100条记录,两个子线程分别打印100条记录,一共300条记录。因为i变量是MyThreadTest的实例属性,而不是局部变量,但因为程序每次创建线程对象时都需要创建一个MyThreadTest对象,所以Thread-0和Thread-1不能共享该实例属性,所以每个线程都将执行100次循环。
两个问题:
- 问题一:我们启动一个线程,必须调用start(),不能调用run()的方式启动线程。
- 问题二:如果再启动一个线程,必须重新创建一个Thread子类的对象,调用此对象的start().
注意点:
- 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
- run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU
调度决定。- 想要启动多线程,必须调用start方法。
- 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上
的异常“IllegalThreadStateException”。
练习:
练习:创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数
public class ThreadDemo {
public static void main(String[] args) {
MyThread1 m1 = new MyThread1();
MyThread2 m2 = new MyThread2();
m1.start();
m2.start();
class MyThread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
class MyThread2 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
方式二:实现Runnable接口
- 1.创建一个实现了Runnable接口的类
- 2.实现类去实现Runnable中的抽象方法:run()
- 3.创建实现类的对象
- 4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
- 5.通过Thread类的对象调用start()
//1. 创建一个实现了Runnable接口的类
class MThread implements Runnable{
//2. 实现类去实现Runnable中的抽象方法:run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
//3. 创建实现类的对象
MThread mThread = new MThread();
//4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread t1 = new Thread(mThread);
t1.setName("线程1");
//5. 通过Thread类的对象调用start():① 启动线程 ②调用当前线程的run()-->调用了Runnable类型的target的run()
t1.start();
//再启动一个线程,遍历100以内的偶数
Thread t2 = new Thread(mThread);
t2.setName("线程2");
t2.start();
}
}
public class MyRunnableTest implements Runnable {
private int i;
void print(){
System.out.println(Thread.currentThread().getName() + "" + i);
}
// run方法同样是线程执行体
public void run() {
for (; i < 100; i++) {
// 当线程类实现Runnable接口时,
// 如果想获取当前线程,只能用Thread.currentThread()方法。
print();
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "" + i);
if (i == 20) {
MyRunnableTest st = new MyRunnableTest();
// 通过new Thread(target , name)方法创建新线程
new Thread(st, "新线程-1").start();
new Thread(st, "新线程-2").start();
}
}
}
}
从该运行结果中我们可以看出,控制台上输出的内容是乱序的,而且每次结果不尽相同。这是因为:
- 在这种方式下,程序所创建的Runnable对象只是线程的target,而多个线程可以共享同一个target。
- 所以多个线程可以共享同一个线程类即线程的target类的实例属性。
- 往控制台窗口print()输出的过程并不是多线程安全的,在一个线程输出过程中另一个线程也可以输出。
为能够保证顺序输出,我们可以对打印方法设置Synchronized,让每次只能有一个进程能够访问打印,代码如下:
public class MyRunnableTest implements Runnable {
private int i;
synchronized void print(){
System.out.println(Thread.currentThread().getName() + "" + i);
}
// run方法同样是线程执行体
public void run() {
for (; i < 100; i++) {
// 当线程类实现Runnable接口时,
// 如果想获取当前线程,只能用Thread.currentThread()方法。
print();
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "" + i);
if (i == 20) {
MyRunnableTest st = new MyRunnableTest();
// 通过new Thread(target , name)方法创建新线程
new Thread(st, "新线程-1").start();
new Thread(st, "新线程-2").start();
}
}
}
}
比较创建线程的两种方式
- 开发中:优先选择:实现Runnable接口的方式
- 原因:
- 1.实现的方式没有类的单继承性的局限性
- 2.实现的方式更适合来处理多个线程有共享数据的情况。
- 联系:public class Thread implements Runnable
- 相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
方式三:JDK 5.0新增线程创建方式–实现Callable接口
实现方法:
- 创建一个实现Callable的实现类
- 实现call方法,将此线程需要执行的操作声明在call()中
- 创建Callable接口实现类的对象
- 将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
- 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
- 获取Callable中call方法的返回值
//1.创建一个实现Callable的实现类
class NumThread implements Callable {
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for(int i = 1;i<=10;i++){
System.out.println(i);
sum+=i;
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args){
//3.创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
//FutureTask 同时实现了Runnable, Future接口
FutureTask futureTask = new FutureTask(numThread);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法
new Thread(futureTask).start();
try {
//6.获取Callable中call方法的返回值,不需要返回值可以不调
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
- call()可以返回值的。
- call()可以抛出异常,被外面的操作捕获,获取异常的信息
- Callable是支持泛型的
Future接口概述
Java 5提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口和Runnable接口可以作为Thread类的target。在Future接口里定义了如下几个公共方法来控制它关联的Callable任务
- 1)boolcan cancel(boolean maylnterruptltRunning):试图取消该Future里关联的Callable任务
- 2)V get():返回Callable任务里call()方法的返回值。调用该方法将导致程序阻塞,必须等到子线程结束后才会得到返回值
- 3)V get(long timeout,TimeUnit unit):返回Callable任务里call()方法的返回值。该方法让程序最多阻塞timeout和unit指定的时间,如果经过指定时间后Callable任务依然没有返回值, 将会抛出TimeoutExccption异常
- 4)boolean isCancelled():如果在Callable任务正常完成前被取消,则返回true
- 5)boolean isDone():妇果Callable任务已完成,则返回true
注意:Callable接口有泛型限制,Callable接口里的泛型形参类型与call()方法返回值类型相同。
方式四:JDK 5.0新增线程创建方式–使用线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程对性能影响很大。
解决方案:
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
实现方法:
- 提供指定线程数量的线程池
- 执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
- 关闭连接池
相关API:
JDK 5.0起提供了线程池相关AP|: Executor Service和 Executors
Executor Service:真正的线程池接口。常见子类 Thread Poolexecutor
void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
<T> Future<T> submit(Callable<T>task):执行任务,有返回值,一般又来执行Callable
void shutdown():关闭连接池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
Executors. newCachedThreadPool():创建一个可根据需要创建新线程的线程池
Executors.newFⅸedthreadPool(n);创建一个可重用固定线程数的线程池
EXecutors. newSingleThreadEXecutor():创建一个只有一个线程的线程池
Executors. new thread Poo(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0;i<10;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
class Number2Thread implements Callable {
@Override
public Object call() throws Exception {
int sum = 0;
for(int i = 1;i<=10;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
sum+=i;
}
return sum;
}
}
public class ThreadPool {
public static void main(String[] args) {
//1. 提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);//创建一个可重用固定线程数为10的线程池
//查看该对象是哪个类造的
System.out.println(service.getClass());//class java.util.concurrent.ThreadPoolExecutor
//设置线程池的属性
// service1.setCorePoolSize(15);
// service1.setKeepAliveTime();
//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());//适合适用于Runnable
Future future = service.submit(new Number2Thread());//适合使用于Callable
try {
System.out.println(future.get());//输出返回值
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
//3.关闭连接池
service.shutdown();
}
}
应用线程池的好处:
- 1.提高响应速度(减少了创建新线程的时间)
- 2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 3.便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没任务时最多保持多长时间后会终止
测试Thread中的常用方法:
方法 | 描述 |
---|---|
public void start() | 使该线程开始执行;Java 虚拟机调用该线程的 run 方法 |
public void run() | 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中。如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。 |
public static Thread currentThread() | 返回对当前正在执行的线程对象的引用。 |
public final void setName(String name) | 改变线程名称,使之与参数 name 相同 |
public Thread(String name) | Thread的有参构造方法 ,创建线程的时候设置名字 |
public static void yield() | 暂停当前正在执行的线程对象,并执行其他线程。 |
public static void sleep(long millisec) | 让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。 |
public final void join() | 在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。 |
public final void join(long millisec) | 等待该线程阻塞的时间最长为 millis 毫秒。 |
public final boolean isAlive() | 测试线程是否处于活动状态。 |
public static boolean holdsLock(Object x) | 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。 |
public void interrupt() | 中断线程。 |
public static void dumpStack() | 将当前线程的堆栈跟踪打印至标准错误流。 |
public final void setPriority(int priority) | 更改线程的优先级 |
public final void setDaemon(boolean on) | 将该线程标记为守护线程或用户线程。 |
部分方法测试:
1:currentThread()、getName()和setName()
2:Thread(String name)和yield()
调用 yield 会让当前线程从 Running 进入 Runnable 状态,即让出 cpu 的使用权, 与其他同样是 Runnable 状态的线程一齐竞争 cpu 的使用权。
class HelloThread extends Thread{
public HelloThread(String name) {
super(name);
}
public HelloThread(){
}
@Override
public void run(){
for(int i = 0;i<=5;i++){
System.out.println(Thread.currentThread().getName()+"---"+i);
}
}
}
public class ThreadMethodTest {
public static void main(String[] args) {
//创建线程的时候设置名字,需要在子类中定义一个有参的构造方法并通过super调用父类有参的构造方法
HelloThread helloThread = new HelloThread("线程一");
helloThread.start();
for(int i = 0;i<=5;i++){
System.out.println(Thread.currentThread().getName()+"---"+i);
if(i == 2){
// 当i为2时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行)
Thread.yield();
}
}
}
}
3:sleep()
调用 Thread.sleep() 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞),睡眠结束线程恢复执行(但是不一定会立刻执行)
class HelloThread extends Thread{
public HelloThread(String name) {
super(name);
}
public HelloThread(){
}
@Override
public void run(){
for(int i = 0;i<=3;i++){
System.out.println(Thread.currentThread().getName()+"---"+i);
}
}
}
public class ThreadMethodTest {
public static void main(String[] args) {
//创建线程的时候设置名字,需要在子类中定义一个有参的构造方法并通过super调用父类有参的构造方法
HelloThread helloThread = new HelloThread("线程一");
helloThread.start();
for(int i = 0;i<=3;i++){
System.out.println(Thread.currentThread().getName()+"---"+i);
try {
//设置进入1000毫秒阻塞状态
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4:join()
在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
class HelloThread extends Thread{
public HelloThread(String name) {
super(name);
}
public HelloThread(){
}
@Override
public void run(){
for(int i = 0;i<=3;i++){
System.out.println(Thread.currentThread().getName()+"---"+i);
}
}
}
public class ThreadMethodTest {
public static void main(String[] args) {
//创建线程的时候设置名字,需要在子类中定义一个有参的构造方法并通过super调用父类有参的构造方法
HelloThread helloThread = new HelloThread("线程一");
helloThread.start();
for(int i = 0;i<=3;i++){
System.out.println(Thread.currentThread().getName()+"---"+i);
if(i == 2){
try {
//此时主线程等待线程一的计算完成,主线程再运行
helloThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
5:isAlive()
测试线程是否处于活动状态。返回false、true
class HelloThread extends Thread{
public HelloThread(String name) {
super(name);
}
public HelloThread(){
}
@Override
public void run(){
System.out.println(Thread.currentThread().getName()+"正在执行");
}
}
public class ThreadMethodTest {
public static void main(String[] args) {
//创建线程的时候设置名字,需要在子类中定义一个有参的构造方法并通过super调用父类有参的构造方法
HelloThread helloThread = new HelloThread("线程一");
helloThread.start();
System.out.println(helloThread.getName()+"的状态:"+helloThread.isAlive());
System.out.println(Thread.currentThread().getName()+"正在执行");
try {
//helloThread线程开始执行,主线程等待
helloThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
//此时线程一的已经执行完,状态已经死亡
System.out.println(helloThread.getName()+"的状态:"+helloThread.isAlive());
System.out.println(Thread.currentThread().getName()+"的状态:"+Thread.currentThread().isAlive());
}
}
线程的优先级
1:等级
- MAX_PRIORITY:10
- MIN _PRIORITY:1
- NORM_PRIORITY:5 -->默认优先级
2:如何获取和设置当前线程的优先级:
- getPriority():获取线程的优先级
- setPriority(int p):设置线程的优先级
说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。
class HelloThread extends Thread{
public HelloThread(String name) {
super(name);
}
public HelloThread(){
}
@Override
public void run(){
System.out.println(Thread.currentThread().getName()+"正在执行");
}
}
public class ThreadMethodTest {
public static void main(String[] args) {
//创建线程的时候设置名字,需要在子类中定义一个有参的构造方法并通过super调用父类有参的构造方法
HelloThread helloThread = new HelloThread("分线程");
//默认分线程和主线程的优先级都是5
System.out.println(helloThread.getName()+"的优先级:"+helloThread.getPriority());
System.out.println(Thread.currentThread().getName()+"的优先级:"+Thread.currentThread().getPriority());
helloThread.setPriority(Thread.MAX_PRIORITY);
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
System.out.println("-----");
//设置分线程和主线程的优先级
System.out.println(helloThread.getName()+"的优先级:"+helloThread.getPriority());
System.out.println(Thread.currentThread().getName()+"的优先级:"+Thread.currentThread().getPriority());
helloThread.start();
System.out.println(Thread.currentThread().getName()+"正在执行");
}
}
三:线程生命周期
四:线程的同步
- 为什么要使用同步?看下面这个例子:
/**
* 例子:创建三个窗口卖票,总票数为10张.使用实现Runnable接口的方式
*/
class WindowThread implements Runnable{
private int ticket = 10;
@Override
public void run() {
while(true){
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+":买票,票号为:"+ticket);
ticket--;
}else{
break;
}
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
WindowThread windowThread = new WindowThread();
Thread t1 = new Thread(windowThread);
Thread t2 = new Thread(windowThread);
Thread t3 = new Thread(windowThread);
t1.start();
t2.start();
t3.start();
}
}
1、线程安全问题存在的原因:
由于一个线程在操作共享数据过程中,未执行完毕的情况下,另外的线程参与进来,导致共享数据存在了安全问题。
2、如何解决线程安全问题
必须让一个线程操作共享数据完毕以后,其它线程才有机会参与共享数据的操作。
3、java如何实现线程安全:线程的同步机制
方式一:同步代码块
synchronized(同步监视器){
//需要被同步的代码块(即为操作共享数据的代码)
}
1、共享数据:多个线程共同操作的同一个数据(变量)
2、同步监视器:由任何一个类的对象来充当。哪个线程获取此监视器,谁就执行大括号里被同步的代码。俗称:锁。
要求:多个线程必须要共用同一把锁。
3、在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
4:、在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。
一:使用同步代码块解决实现Runnable接口的线程安全问题
class Window2 implements Runnable {
int ticket = 100;// 共享数据
public void run() {
while (true) {
synchronized (this) {
//this表示当前对象,本题中即为w
if (ticket > 0) {
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "售票,票号为:" + ticket--);
}
}
}
}
}
public class TestWindow2 {
public static void main(String[] args) {
Window2 w = new Window2();
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();
}
}
class Animal{
}
二:使用同步代码块解决继承Thread类的方式的线程安全问题
二:使用同步代码块解决继承Thread类的方式的线程安全问题
* 例子:创建三个窗口卖票,总票数为100张.使用继承Thread类的方式
* 说明:在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。
class Window2 extends Thread{
private static int ticket = 100;
private static Object obj = new Object();
@Override
public void run() {
while(true){
//正确的
// synchronized (obj){
synchronized (Window2.class){
//Class clazz = Window2.class,Window2.class只会加载一次
//错误的方式:this代表着t1,t2,t3三个对象
// synchronized (this){
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":卖票,票号为:" + ticket);
ticket--;
}else{
break;
}
}
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
Window2 t1 = new Window2();
Window2 t2 = new Window2();
Window2 t3 = new Window2();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
方式二:同步方法
如果操作共享数据的代码完整的声明在一个方法中,可以将其定义为同步方法。
例如:
public synchronized void show (String name){
….
}
一:使用同步方法解决实现Runnable接口的线程安全问题
一:使用同步方法解决实现Runnable接口的线程安全问题
* 关于同步方法的总结:
* 1. 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
* 2. 非静态的同步方法,同步监视器是:this
* 静态的同步方法,同步监视器是:当前类本身
class WindowThread3 implements Runnable{
private int ticket = 10;
@Override
public void run() {
while(true){
show();
}
}
//同步show方法,继承Thread类方法一样,只需同步方法即可,同时需要给方法加static关键字,确保不会创建多个对象
private synchronized void show(){
//同步监视器默认为this
if(ticket > 0){
System.out.println(Thread.currentThread().getName()+":买票,票号为:"+ticket);
ticket--;
}
}
}
public class WindowTest3 {
public static void main(String[] args) {
WindowThread3 windowThread3 = new WindowThread3();
Thread t1 = new Thread(windowThread3);
Thread t2 = new Thread(windowThread3);
Thread t3 = new Thread(windowThread3);
t1.start();
t2.start();
t3.start();
}
}
二:使用同步方法处理继承Thread类的方式中的线程安全问题
class Window4 extends Thread{
private static int ticket = 10;
@Override
public void run() {
//买票操作
while (true) {
show();
}
}
//生命成静态synchronized方法
private static synchronized void show(){
//同步监视器:Window4.class
//private synchronized void show(){ //同步监视器不唯一。此种解决方式是错误的
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":买票,票号为:" + ticket);
ticket--;
}
}
}
public class WindowTest4 {
public static void main(String[] args) {
Window4 w1 = new Window4();
Window4 w2 = new Window4();
Window4 w3 = new Window4();
w1.start();
w2.start();
w3.start();
}
}
方式三:Lock锁 — JDK 5.0新增
- 从JDK 5.0开始,Java提供了更强大的线程同步机制–通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
- ReentrantLock类实现了Lock,它拥有与 synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 Reentrantlock,可以显式加锁、释放锁。
class A {
//1.实例化ReentrantLock对象
private final ReenTrantLock lock = new ReenTrantLook();
public void m (){
lock.lock//2.先加锁
try{
//保证线程同步的代码
}finally{
lock.unlock();//3.后解锁
}
}
}
//注意:如果同步代码块有异常,要将unlock()写入finally语句块中
代码示例:
class Window implements Runnable{
private int ticket = 10;
//1.实例化ReentrantLock,如果使用继承的方式则ReentrantLock对象必须为静态的的
private ReentrantLock lock = new ReentrantLock();//默认构造方法的参数为false
//private ReentrantLock lock = new ReentrantLock(true);参数为true表示公平锁,就是谁等的时间最长,谁就先获取锁
@Override
public void run() {
while(true){
try{
//2.调用锁定方法lock()
lock.lock();
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":买票,票号为:"+ticket);
ticket--;
}else{
break;
}
}finally {
//3.3.调用解锁方法:unlock()
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.start();
t2.start();
t3.start();
}
}
1、synchronized 与 Lock 的对比
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放。
- Lock只有代码块锁,synchronized有代码块锁和方法锁。
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。
优先使用顺序:
Lock → 同步代码块(已经进入了方法体,分配了相应资源) → 同步方法(在方法体之外)
4、处理单例模式之懒汉式的线程安全问题
1: 使用同步机制将单例模式中的懒汉式改写为线程安全的
public class BankTest {
}
class Bank{
private Bank(){
}
private static Bank instance = null;
public static Bank getInstance(){
//方式一:效率稍差,当第一个线程创建了对象后,其他线程就没必要再进去了。
// synchronized (Bank.class) {
// if(instance == null){
//
// instance = new Bank();
// }
// return instance;
// }
//方式二:效率更高
if(instance == null){
synchronized (Bank.class) {
if(instance == null){
instance = new Bank();
}
}
}
return instance;
}
}
5、死锁问题
定义
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
解决方法
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
举例
public class ThreadTest {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread(){
@Override
public void run() {
synchronized (s1){
s1.append("a");
s2.append("1");
try {
//如果先执行该线程,此处将其阻塞,之后有可能执行另一个线程。
//则s1和s2同时被上锁,就会出现死锁的状况
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2){
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2){
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
6、练习
* 银行有一个账户。
有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
分析:
1.是否是多线程问题? 是,两个储户线程
2.是否有共享数据? 有,账户(或账户余额)
3.是否有线程安全问题?有
4.需要考虑如何解决线程安全问题?同步机制:有三种方式。
class Account{
private double balance;
public Account(double balance) {
this.balance = balance;
}
//存钱
public synchronized void deposit(double amt){
if(amt > 0){
balance += amt;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":存钱成功。余额为:" + balance);
}
}
}
class Customer extends Thread{
private Account acct;
public Customer(Account acct) {
this.acct = acct;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
acct.deposit(1000);
}
}
}
public class AccountTest {
public static void main(String[] args) {
Account acct = new Account(0);
Customer c1 = new Customer(acct);
Customer c2 = new Customer(acct);
c1.setName("甲");
c2.setName("乙");
c1.start();
c2.start();
}
}
五:线程的通信
1. 三种方法
-
wait():一旦执行该方法,当前进程就进入阻塞状态,并释放同步监视器(与sleep不同的一点)。
-
notify():唤醒被wait的线程中优先级最高者。(唤醒一个)
-
notifyAll ():唤醒被wait的所有线程。(唤醒所有)
- 这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常。
- 这三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
- 因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明。
代码实例:
使用两个线程打印 1-100,线程1, 线程2 交替打印
class Number implements Runnable{
private int number = 1;
Object obj = new Object();
@Override
public void run() {
while(true){
synchronized(obj){
obj.notify();//notify()方法唤醒线程,notifyAll()唤醒所有线程
if(number <= 10){
System.out.println(Thread.currentThread().getName()+"打印了:"+number);
number++;
try {
obj.wait();//wait()方法将线程阻塞,同时释放该线程的锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
break;
}
}
}
}
}
public class ConmmunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.start();
t2.start();
}
}
生产者消费者问题
class Clerk{
private int num;//产品数量
//生产产品
public synchronized void produceProduct() {
if(num < 20){
num++;
System.out.println(Thread.currentThread().getName() + ":开始生产第" + num + "个产品");
notify();//生产者
}else{
//当产品数量大于20时,生产者阻塞
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消费产品
public synchronized void consumeProduct() {
if(num>0){
System.out.println(Thread.currentThread().getName() + ":开始消费第" + num + "个产品");
num--;
notify();//消费者消费完产品后唤醒生产者
}else{
//没有产品时,消费者阻塞
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//生产者类
class Producer extends Thread{
private Clerk clerk;
public Producer(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
//生产产品
while(true){
clerk.produceProduct();
}
}
}
class Consumer extends Thread{
private Clerk clerk;
public Consumer(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
//消费者消费产品
while(true){
clerk.consumeProduct();
}
}
}
public class ProductTest {
public static void main(String[] args){
Clerk clerk = new Clerk();
Producer p = new Producer(clerk);
Consumer c = new Consumer(clerk);
p.setName("生产者");
c.setName("消费者");
p.start();
c.start();
}
}
2. wait 和 sleep 方法的异同
- 相同点:
(1)一旦执行方法,都可以使得当前的线程进入阻塞状态。- - 不同点:
(1)两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait()。
(2)调用的要求不同:sleep()可以在任何需要的场景下调用,wait()必须使用在同步代码块或同步方法中。
(3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放同步监视器,而wait()会释放同步监视器。