一、并发和并行
并发:同一时刻,多个指令在单个CPU上交替执行。
并行:同一时刻,多个指令在多个CPU上同时执行。
二、多线程的实现方式
1. 继承Thread类的方式进行实现。
public class ThreadDemo {
public static void main ( String[] args ) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
// 3.创建子类的对象,并启动线程
t1.setName( "线程1" );
t2.setName( "线程2" );
t1.start();
t2.start();
}
}
public class MyThread extends Thread{
// 1.定义一个类继承Thread
public void run() {
// 2.重写run方法
for (int i = 1; i <= 10; i++) {
System.out.println( "Hello World " + getName() );
}
}
}
运行后可以发现,两个线程交替执行,即为并发。
2. 实现Runnable接口的方式进行实现。
public class ThreadDemo {
public static void main ( String[] args ) {
MyRun mr = new MyRun( );
// 3.创建类的对象
Thread t1 = new Thread( mr );
Thread t2 = new Thread( mr );
// 4.创建Thread对象,并开启线程
t1.setName( "线程1" );
t2.setName( "线程2" );
t1.start( );
t2.start( );
}
}
public class MyRun implements Runnable {
// 1.自己定义一个类实现Runnable接口
@Override
public void run ( ) {
// 2.重写run方法
for ( int i = 1; i <= 10; i++ ) {
System.out.println( "Hello World " + Thread.currentThread( ).getName( ) );
}
}
}
实现Runnable接口的类无法直接调用getName()方法,但是可以通过currentThread()方法获取当前线程的对象,再调用getName()方法。
3. 利用Callable接口和Future接口方式实现
第三种方法是对前面两种方法的补充,比前面两种方法多了一个返回值,获取多线程运行的结果。
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
public class ThreadDemo {
public static void main ( String[] args ) throws ExecutionException, InterruptedException {
MyCall mc = new MyCall( );
// 3.创建MyCall的对象(表示多线程要执行的任务)
FutureTask< Integer > ft = new FutureTask<>( mc );
// 4.创建FutureTask的对象(管理多线程的结果)
Thread t1 = new Thread( ft );
// 5.创建Thread的对象,并启动
t1.start( );
Integer res = ft.get( );
System.out.println( res );
}
}
import java.util.concurrent.Callable;
public class MyCall implements Callable {
// 1.创建一个类实现Callable接口
public Integer call ( ) {
// 2.重写call方法
Integer sum = 0;
for ( int i = 1; i <= 10; i++ ) {
sum += i;
}
return sum;
}
}
4.多线程三种实现方式对比
三、多线程中常用的成员方法
-
String getName() :返回线程名称
-
void setName(String name) :设置线程名(构造方法也可以设置名字)
-
static Thread currentThread() :获取当前线程的对象
-
static void sleep(long time) :让线程休眠time毫秒,休眠时间一到,线程立即恢复可运行状态(就绪),注意不是运行状态
-
setPriority(int newPriority) :设置线程的优先级
-
final int getPriority() :获取线程的优先级
-
final void setDaemon(boolean on) :设置为守护线程,JAVA中的线程主要分为两类:用户线程和守护线程,在其他非守护线程执行完毕之后,守护线程就会陆续结束(不是直接结束)。
-
public static void yield() :礼让线程,让当前正在执行的线程暂停,但不阻塞,即为将线程从运行状态转化为就绪状态,让CPU重新调度。
-
public static void join() :插队线程,使调用当前方法的线程排在当前线程前面,执行完调用当前方法的线程再执行当前线程。
四、线程的调度
- setPriority(int newPriority) :设置线程的优先级
- final int getPriority() :获取线程的优先级
线程的调度主要分为两种:抢占式调度(随机性)和非抢占式调度(所有线程轮流执行),其中对于强制式调度,线程的优先级越高,抢到CPU的概率越大,线程的优先级分为1~10,没有设置就默认为5。
五、线程的生命周期
线程的生命周期中主要经过五种状态,新建、就绪、运行、阻塞和死亡。
六、线程安全问题
当多线程操作同一个数据的时候会出现问题,如下:
public class MyThread extends Thread {
static int ticket = 0;
@Override
public void run ( ) {
while ( true ) {
if ( ticket < 100 ) {
try {
Thread.sleep( 100 );
} catch ( InterruptedException e ) {
throw new RuntimeException( e );
}
}
ticket++;
System.out.println( getName( ) + "正在售卖第" + ticket + "张票!!!" );
}
}
}
1. synchronized
保证线程安全可以使用synchronized关键字(自动锁)。
使用synchronized的条件:
- 必须有两个或两个以上的线程。
- 同一时间只有一个线程能够执行同步代码
- 多个线程想要同步时,必须共用同一把锁,即为synchronized(对象)括号内的对象必须是同一个对象。
使用synchronized的过程:
- 只有抢到锁的线程才能执行同步代码块,其余的线程即使抢到了CPU执行权,也只能等待锁的释放。
- 代码执行完毕或程序抛出异常都会释放锁,然后还未执行同步代码块的线程争抢锁,谁抢到谁就能运行同步代码块。
1.1 同步代码块
synchronized(对象){
//可能会发生线程安全问题的代码
}
//这里的对象可以是任意对象(但是必须是同一个对象),我们可以用 Object obj = new Object()里面的obj放入括号中
public class MyThread extends Thread {
static int ticket = 0;
static Object obj = new Object( );
@Override
public void run ( ) {
while ( true ) {
synchronized ( MyThread.class ) {
if ( ticket < 100 ) {
try {
Thread.sleep( 100 );
} catch ( InterruptedException e ) {
throw new RuntimeException( e );
}
ticket++;
System.out.println( getName( ) + "正在售卖第" + ticket + "张票!!!" );
} else {
break;
}
}
}
}
}
1.2 同步方法
即为把synchronized关键字加到方法上,格式修饰符 synchronized 返回值类型 方法名(方法参数){}
,特点:
- 同步方法是锁住方法里的所有代码
- 锁对象不能自己指定(JAVA已经规定好的),非静态方法的锁对象是this,静态方法的锁对象是当前类字节码文件对象。
public class MyRunnable implements Runnable {
int ticket = 0;
@Override
public void run ( ) {
while ( true ) {
if ( method( ) ) break;
}
}
/**
* ctrl + alt + m选中代码块生成方法
* @return
*/
private synchronized boolean method ( ) {
if ( ticket < 100 ) {
try {
Thread.sleep( 10 );
} catch ( InterruptedException e ) {
throw new RuntimeException( e );
}
ticket++;
System.out.println( Thread.currentThread( ).getName( ) + "正在售卖第" + ticket + "张票!!!" );
} else {
return true;
}
return false;
}
}
1.3 补充StringBuilder和StringBuffer
StringBuilder的实例用于多个线程是不安全的,如果需要这样的同步则使用StringBuffer。
2. lock锁
在JDK5以后提供了一个新的锁对象lock,实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作,lock中提供了获得锁和释放锁的方法(所以可以手动上锁和手动释放)。lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化。
public class MyThread extends Thread {
static int ticket = 0;
static Lock lock = new ReentrantLock( );
@Override
public void run ( ) {
while ( true ) {
lock.lock( );
try {
if ( ticket < 100 ) {
Thread.sleep( 1000 );
ticket++;
System.out.println( getName( ) + "正在售卖第" + ticket + "张票!!!" );
} else {
break;
}
} catch ( InterruptedException e ) {
throw new RuntimeException( e );
} finally {
// 在break之后也要关锁
lock.unlock( );
}
}
}
}
七、死锁
在多线程编程中,防止多线程竞争共享资源导致数据错乱,我们会在操作共享资源之前加上互斥锁,只有成功拿到锁的线程,才能操作共享数据,获取不到锁的线程只能等待,直到锁被释放。
死锁是指在一组进程中,每个进程都在等待其他进程释放资源,而这些资源又被这组进程中的其他进程占有,导致所有进程都无法向前推进的状态。这种情况下,进程无限期地等待一个永远不会发生的事件,从而系统资源得不到有效利用,甚至可能导致系统崩溃。
产生死锁的条件:
- 互斥条件:进程对所分配到的资源具有排他性,即一次只有一个进程能使用。
- 请求与保持条件:继承至少已经持有一个资源但又提出新的资源请求,而该资源已被其他进程占有。
- 不可抢占条件:进程已经获得的资源在未使用完之前,不得被其他进程强行夺走。
- 循环等待条件:在发生死锁时,必然存在一个进程-资源的环形链。
模拟代码如下:
public class MyThread extends Thread {
static Object obj1 = new Object( );
static Object obj2 = new Object( );
@Override
public void run ( ) {
while ( true ) {
if ( "a".equals( getName( ) ) ) {
synchronized ( obj1 ) {
System.out.println( "a拿到了锁A" );
synchronized ( obj2 ) {
System.out.println( "a拿到了锁B,执行完一轮" );
}
}
} else if ( "b".equals( getName( ) ) ) {
synchronized ( obj2 ) {
System.out.println( "b拿到了锁B" );
synchronized ( obj1 ) {
System.out.println( "b拿到锁A,执行完一轮" );
}
}
}
}
}
}
八、等待唤醒机制
等待唤醒机制是多线程之间的协作机制,当一个线程执行完规定操作后,进入等待状态,等待其他线程执行完自己的指定代码后再来唤醒进入刚刚进入等待的线程。
1.直接实现
2.阻塞队列实现等待唤醒机制
import java.util.concurrent.ArrayBlockingQueue;
public class ThreadDemo {
public static void main ( String[] args ) {
ArrayBlockingQueue<String>queue = new ArrayBlockingQueue<>( 1 );
// 创建阻塞队列对象
// ArrayBlockingQueue底层是数组,所以需要指定队列长度
Cook cook = new Cook( queue );
Foodie foodie = new Foodie( queue );
// 创建线程对象,并传入阻塞队列
cook.start();
foodie.start();
// 开启线程
}
}
import java.util.concurrent.ArrayBlockingQueue;
public class Cook extends Thread {
ArrayBlockingQueue< String > queue;
public Cook ( ArrayBlockingQueue< String > queue ) {
this.queue = queue;
}
@Override
public void run ( ) {
while ( true ) {
try {
queue.put( "面条" );
// 不需要使用锁,队列内部有锁,锁被拿到后,队列会一直判断队列是否是满的,当队列
// 不是满的的时候,会结束循环,然后释放锁
System.out.println( "厨师制作了一碗面条" );
} catch ( InterruptedException e ) {
throw new RuntimeException( e );
}
}
}
}
import java.util.concurrent.ArrayBlockingQueue;
public class Foodie extends Thread {
ArrayBlockingQueue queue;
public Foodie ( ArrayBlockingQueue queue ) {
this.queue = queue;
}
@Override
public void run ( ) {
while ( true ) {
try {
String food = (String) queue.take( );
// take方法底层同样有锁,所以外面就不用加锁了,锁的嵌套容易导致死锁
System.out.println( "客人吃了一碗面" );
} catch ( InterruptedException e ) {
throw new RuntimeException( e );
}
}
}
}
此时会发现输出的结果可能会出现连续两个厨师做面条,因为输出语句不在锁内,释放锁之后,可能会导致CPU被另一个线程抢到,所以可能会先输出客人吃面条再输出厨师做面条。
锁的嵌套容易导致死锁,所以不能随便嵌套锁。
九、线程的状态
在JAVA(虚拟机)中,线程有六个状态:NEW(新建)、RUNNABLE(就绪)、BLOCKED(阻塞)、WAITING(等待)、TIMED_WAITING(计时等待)、TERMINATED(死亡)。没有运行状态,因为线程抢到CPU的执行权后,线程就会交给操作系统管理。
- 新建状态(NEW) -> 创建线程对象
- 就绪状态(RUNNABLE) -> start方法
- 阻塞状态(BLOCKED) -> 无法获得锁对象
- 等待状态(WAITING) -> wait方法
- 计时等待(TIMED_WAITING) -> sleep方法
- 结束状态(TERMINATED) -> 全部代码运行完毕