程序、进程、线程
程序:是为了完成特定任务,用某种语言编写的一组指令的集合,是一段静态的代码。(程序是静态的)
进程:是程序的一次动态执行。正在运行的一个程序,进程作为资源分配的单位,在内存中会为每个进程分配不同的内存区域。(进程是动态的),进程的生命周期:有它自身的产生、存在和消亡的过程。
线程:进程可进一步细化为线程,是一个程序内部的一条执行路径。若一个进程同一时间并行执行多个线程,就是支持多线程的。
单核CPU与多核CPU
单核CPU :CPU 在执行的时候,是按照时间片执行的,同一个时间片只能执行一个任务。执行多个任务时,会在多个任务之间来回跳。时间片切换很快,给人感觉像是在同时执行多个线程。是一种假象。
多核CPU :多个CPU 的时候,才真正意义上做到一个时间片多线程同时执行。多线程发挥了最好的效率。真正的多线程。
并行和并发
并行:多个CPU 同时执行多个任务。
并发:一个CPU “同时”执行多个任务。(采用时间片切换)
在学习多线程之前,以前的代码都是单线程吗?
不是,以前的代码也是多线程。除了main 方法为主线程,还包括处理异常的线程和垃圾回收的线程。PS:处理异常的线程会影响主线程的执行。
创建线程的三种方式
1. 继承Thread类
public class BuyTicketThread extends Thread {
public BuyTicketThread (String name) {
super (name);
}
static int ticketNum = 10 ;
@Override
public void run () {
for (int i = 1 ; i <= 100 ; i++) {
if (ticketNum>0 ){
System.out.println("我在" +this .getName()+"抢到了从北京到邯郸的第" + ticketNum-- +"张票" );
}
}
}
}
public class Test {
public static void main (String[] args) {
BuyTicketThread buy1 = new BuyTicketThread ("窗口1" );
buy1.start();
BuyTicketThread buy2 = new BuyTicketThread ("窗口2" );
buy2.start();
BuyTicketThread buy3 = new BuyTicketThread ("窗口3" );
buy3.start();
}
}
2.实现Runnable接口
public class BuyTicketThread implements Runnable {
int ticketNum = 10 ;
@Override
public void run () {
for (int i = 1 ; i <= 10 ; i++) {
if (ticketNum > 0 ){
System.out.println("我在" +Thread.currentThread().getName()+"买到了第" + ticketNum-- +"张北京到邯郸的车票" );
}
}
}
}
public class Test {
public static void main (String[] args) {
BuyTicketThread buy = new BuyTicketThread ();
Thread thread1 = new Thread (buy,"窗口1" );
thread1.start();
Thread thread2 = new Thread (buy,"窗口2" );
thread2.start();
Thread thread3 = new Thread (buy,"窗口3" );
thread3.start();
}
}
3.实现Callable接口
对比方式1 和方式2 ,都需要有一个run方法,但是这个run方法有不足之处:没有返回值,不能抛出异常。
基于上面两个不足,在JDK1.5 之后出现了第三种创建多线程的方式:实现Callable接口:有返回值,可以抛出异常,但是创建比较麻烦。
public class TestCallable implements Callable <Integer> {
@Override
public Integer call () throws Exception {
return new Random ().nextInt(10 );
}
}
public class Test {
public static void main (String[] args) throws ExecutionException, InterruptedException {
TestCallable tc = new TestCallable ();
FutureTask ft = new FutureTask (tc);
Thread thread = new Thread (ft);
thread.start();
Object obj = ft.get();
System.out.println(obj);
}
}
方式1继承Thread类和方式2实现Runnable接口,实际开发中哪个方式更好?
方式1 在Java单继承中具有局限性,继承了Thread类,就不能继承其他类了。
方式2 的共享资源能力更强一些,变量不用加static 来修饰也可以实现共享,方式1 共享变量必须用static 修饰。(为什么不用static 也可以) https:
Thread类和Runnable接口有什么关系?
Runnable接口
线程的生命周期
新生状态、就绪状态、执行状态、死亡状态、阻塞状态。
Thread thread = new Thread ();新生状态-----start启动----》就绪状态(等待CPU调度)----run()获取cpu执行权--------》运行状态-------正常结束/出现异常/调用stop方法(不建议,该方法已废弃)----------》死亡状态
Thread thread = new Thread ();新生状态-----start启动----》就绪状态(等待CPU调度)----run()获取cpu执行权--------》运行状态-------阻塞事件----------》阻塞状态-------阻塞事件解除----------------》就绪状态
Thread thread = new Thread ();新生状态-----start启动----》就绪状态(等待CPU调度)----run()获取cpu执行权--------》运行状态------调用wait方法----》等待队列-----调用notify/notifyall方法-------》锁池状态-------释放锁-》就绪状态
Thread thread = new Thread ();新生状态-----start启动----》就绪状态(等待CPU调度)----run()获取cpu执行权--------》运行状态------synchronized ----》锁池状态-----释放锁-----》就绪状态
线程的常见方法
start():启动当前线程,表面上调用start方法,实际上执行run方法。
run ():线程类继承Thread或者实现runnable接口的时候,都要重写run方法,run方法里面是线程要执行的内容。
currentThread:Thread类中的一个静态方法,获取正在执行的线程。
setName ():设置线程名称
getName ():获取线程名称
setPriority (int num):设置优先级别方法:同优先级别的线程采取先到先服务,使用时间片策略。如果优先级高,被CPU调度的概率就高。级别为:1 -10 ,默认是5 。
public final static int MIN_PRIORITY = 1 ;
public final static int NORM_PRIORITY = 5 ;
public final static int MAX_PRIORITY = 10 ;
设置优先级别
public class TestThread01 extends Thread {
@Override
public void run() {
for (int i = 0 ; i < 10 ; i++) {
System .out.println(i);
}
}
}
public class TestThread02 extends Thread {
@Override
public void run() {
for (int i = 20 ; i < 30 ; i++) {
System .out.println(i);
}
}
}
public class Test {
public static void main(String [] args) {
TestThread01 thread01 = new TestThread01 ();
thread01.setPriority(10 );
thread01.start();
TestThread02 thread02 = new TestThread02 ();
thread02.setPriority(2 );
thread02.start();
}
}
join方法
当一个线程调用了join 方法,这个线程就会先被执行,它执行结束以后才可以去执行其余的线程。注意:必须先start,再join 才有效。
public class TestThreadJoin extends Thread {
public TestThreadJoin (String name ) {
super(name);
}
@Override
public void run () {
for (int i = 0 ; i < 10 ; i++) {
System.out .println(this .getName()+"----------" +i);
}
}
}
public class Test {
public static void main (String[] args ) throws InterruptedException {
for (int i = 0 ; i < 100 ; i++) {
System.out .println("main-------------" +i);
if (i==6 ){
TestThreadJoin ttj = new TestThreadJoin("join子线程" );
ttj.start();
ttj.join ();
}
}
}
}
sleep方法
sleep人为制造阻塞事件
public class Test {
public static void main (String[] args) throws InterruptedException {
DateFormat dateFormat = new SimpleDateFormat ("HH:mm:ss" );
Date date = new Date ();
String format = dateFormat.format(date);
System.out.println(format);
Thread.sleep(3000 );
}
}
设置伴随线程
将子线程设置为主线程的伴随线程,主线程停止的时候,子线程也不要继续执行了。不会立马结束,会垂死挣扎一会。
public class TestThread extends Thread{
@Override
public void run () {
for (int i = 0 ; i < 100 ; i++) {
System.out.println ("子线程--" +i);
}
}
}
class Test {
public static void main (String[] args) {
TestThread thread = new TestThread ();
thread.setDaemon (true );
thread.start ();
for (int i = 0 ; i < 10 ; i++) {
System.out.println ("main---" +i);
}
}
}
stop方法
public class TestStop {
public static void main (String[] args ) {
for (int i = 0 ; i < 100 ; i++) {
if (i==6 ){
Thread.currentThread().stop();
}
System.out .println(i);
}
}
}
同步代码块
买票代码出现的问题:
1. 出现了两个或多个10 张票。因为ticketNum--时,执行了两个操作,一个是打印ticketNum,一个是ticketNum减1 ,很可能在ticketNum没有执行减1 操作时,被其他线程抢到了资源先打印出来,导致多售票的现象发生。
2. 出现0 或者-1 ,-2 的情况。跟上述一样的情况,先执行了减1 操作,后执行了打印。
上面代码出现的问题:出现了重票,多票。
原因:多个线程,在争抢资源中,导致共享的资源出现了问题。一个线程没执行完,另一个线程参与进来,开始争抢。
解决:在程序中加入锁。
方式1 :同步代码块 this 锁
public class TestSyncThread implements Runnable {
int ticketNum = 10 ;
@Override
public void run () {
for (int i = 1 ; i <= 100 ; i++) {
synchronized (this ) {
if (ticketNum>0 ){
System.out.println("我在" +Thread.currentThread().getName()+"买到了邯郸到北京的第" + ticketNum-- +"张票" );
}
}
}
}
}
public class Test {
public static void main (String[] args) {
TestSyncThread tst = new TestSyncThread ();
Thread thread = new Thread (tst,"窗口1" );
thread.start();
Thread thread2 = new Thread (tst,"窗口2" );
thread2.start();
Thread thread3 = new Thread (tst,"窗口3" );
thread3.start();
}
}
同步代码块 字节码信息锁
public class TestSyncLockThread extends Thread {
static int ticketNum = 10 ;
@Override
public void run () {
for (int i = 1 ; i <= 100 ; i++) {
synchronized (TestSyncLockThread.class) {
if (ticketNum>0 ){
System.out.println("我在" +Thread.currentThread().getName()+"抢到了从北京到邯郸的第" + ticketNum-- +"张票" );
}
}
}
}
}
public class Test {
public static void main (String[] args) {
Thread thread1 = new TestSyncLockThread ();
thread1.setName("窗口1" );
thread1.start();
Thread thread2 = new TestSyncLockThread ();
thread2.setName("窗口2" );
thread2.start();
Thread thread3 = new TestSyncLockThread ();
thread3.setName("窗口3" );
thread3.start();
}
}
同步监视器总结:synchronized (同步监视器){},是写在括号中的叫做同步监视器。
1. 必须是引用类型,不能是基本数据类型。
2. 也可以创建一个专门的同步监视器,没有任何业务含义。Object o = new Object ();
3. 一般使用共享资源做同步监视器即可。如上面类的字节码。
4. 在同步代码块中不能改变同步监视器的引用。错误示例:String syncStr = "abc" ;synchronized (syncStr){syncStr ="bcd" ;}
5. 尽量不要用String和包装类Integer做同步监视器。
6. 建议使用final 修饰同步监视器。 final String syncStr = "abc" ;
同步代码块的执行过程:
1. 第一个线程来到同步代码块,发现锁处于open状态,先进行close,然后执行代码块中的代码。
2. 第一个线程在执行代码过程中出现了线程切换,(阻塞 就绪),失去了CPU,但是没有开锁open。
3. 第二个线程来到同步代码块,发现锁处于close,无法执行代码块中的代码,第二个线程进入阻塞。
4. 第一个线程重新拿到CPU,执行完代码,释放锁open。
5. 第二个线程也再次拿到CPU,来到同步代码块,发现锁处于open状态,拿到锁并上锁,由阻塞状态变为就绪状态再变为执行状态,重复线程一的过程。
强调:同步代码块中能发生CPU的切换吗?能,但是后续的被执行的线程无法执行代码块中的代码,因为锁仍旧close。
总结:其他
1. 多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,其他线程无法访问其中的任何一个代码块。
2. 多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,但是没有锁住使用其他同步监视器的代码块,其他线程有机会访问其他同步监视器的代码块。
同步方法
public class TestSyncMethod implements Runnable {
int ticketNum = 10 ;
@Override
public void run () {
for (int i = 1 ; i < 100 ; i++) {
buyTicket();
}
}
public synchronized void buyTicket () {
if (ticketNum>0 ){
System.out.println("我在" +Thread.currentThread().getName()+"抢到了第" + ticketNum-- +"张票" );
}
}
}
public class Test {
public static void main (String[] args) {
TestSyncMethod method = new TestSyncMethod ();
Thread thread1 = new Thread (method,"窗口1" );
thread1.start();
Thread thread2 = new Thread (method,"窗口2" );
thread2.start();
Thread thread3 = new Thread (method,"窗口3" );
thread3.start();
}
}
public class TestSyncMethod2 extends Thread {
static int ticketNum = 10 ;
@Override
public void run () {
for (int i = 1 ; i < 100 ; i++) {
buyTicket();
}
}
public static synchronized void buyTicket () {
if (ticketNum>0 ){
System.out.println("我在" +Thread.currentThread().getName()+"抢到了第" + ticketNum-- +"张票" );
}
}
}
public class Test2 {
public static void main (String[] args) {
TestSyncMethod2 method1 = new TestSyncMethod2 ();
method1.setName("窗口1" );
method1.start();
TestSyncMethod2 method2 = new TestSyncMethod2 ();
method2.setName("窗口2" );
method2.start();
TestSyncMethod2 method3 = new TestSyncMethod2 ();
method3.setName("窗口3" );
method3.start();
}
}
总结:关于同步方法
1. 不要将run()定义为同步方法。
2. 非静态同步方法的同步监视器是this 。静态同步方法的同步监视器是类的字节码对象。类.class
3. 同步代码块的效率要高于同步方法。
原因:同步方法是将线程挡在了方法的外部,而同步代码块可以进入方法,只是在代码块的外部,但却是在方法的内部。
4. 同步方法的锁是this ,一旦锁住一个方法,就锁住了所有的同步方法。同步代码块只是锁住使用该同步监视器的代码块,而没有锁住使用其他同步监视器的代码块。
lock锁
JDK1.5 后新增新一代的线程同步方式线程锁,与采用synchronized相比,lock 可提供多种锁方案,更灵活。
synchronized是Java中的关键字,属于虚拟机级别的锁。这个关键字是靠JVM来识别完成的。
Lock锁是API级别的,提供了相应的接口和实现类,这个方式更加灵活,表现出来的性能优于之前的方式。
###### 代码演示:
public class Test {
public static void main (String[] args ) {
TestLock testLock = new TestLock();
Thread thread = new Thread(testLock,"窗口1" );
thread.start();
Thread thread2 = new Thread(testLock,"窗口2" );
thread2.start();
Thread thread3 = new Thread(testLock,"窗口3" );
thread3.start();
}
}
public class TestLock implements Runnable {
int ticketNum = 10 ;
Lock lock = new ReentrantLock();
@Override
public void run () {
for (int i = 1 ; i <= 100 ; i++) {
lock .lock ();
try {
if (ticketNum>0 ){
System.out .println("我在" +Thread.currentThread().getName()+"抢到了第" + ticketNum-- +"张票" );
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock .unlock();
}
}
}
}
lock 锁和synchronized的区别:
1.l ock是显示锁(手动开启和关闭锁,别忘了关闭锁),synchronized是隐式锁。
2.l ock只有代码块锁,synchronized有代码块锁和方法锁。
3. 使用lock 锁,JVM花费较少的时间来调度线程,性能更好。并且具有更好的扩展性。(提供更多的子类)
使用的顺序:
Lock--同步代码块(已经进入了方法体,分配了相应的资源)--同步方法(在方法体之外)
线程同步的优缺点
1. 对比:线程安全,效率低。线程不安全,效率高。
2. 可能造成死锁:
死锁:
>不同的线程分别占用对方需要的同步资源不放弃,都在等待对方释放自己需要的同步资源,就形成了线程的死锁。
>出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
3. 代码演示:
public class Test {
public static void main (String[] args) {
TestDeadLock testDeadLock1 = new TestDeadLock ();
TestDeadLock testDeadLock2 = new TestDeadLock ();
testDeadLock1.flag=1 ;
testDeadLock2.flag=0 ;
Thread thread1 = new Thread (testDeadLock1);
thread1.start();
Thread thread2 = new Thread (testDeadLock2);
thread2.start();
}
}
public class TestDeadLock implements Runnable {
public int flag = 1 ;
static Object o1 = new Object (), o2 = new Object ();
@Override
public void run () {
System.out.println("flag==" +flag);
if (flag == 1 ){
synchronized (o1){
try {
Thread.sleep(500 );
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
System.out.println("o2" );
}
}
}
if (flag==0 ){
synchronized (o2){
try {
Thread.sleep(500 );
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
System.out.println("o1" );
}
}
}
}
}
4. 解决办法:减少同步资源的定义,避免嵌套同步。
线程通信问题
在Java对象中,有两种池
锁池-----------------synchronized
等待池---------------wait (),notify (),notifyall ()
如果一个线程调用了某个对象的wait方法,那么该线程进入到该对象的等待池中(并且已经将锁释放),
如果未来某个时刻,另外一个线程调用了相同对象的notify方法或者notifyall方法,
那么该等待池中的线程就会被唤醒,然后进入到该对象的锁池里面获取该对象的锁,
如果获得锁成功后,那么该线程就会沿着wait方法之后的路径继续执行。注意是沿着wait()方法之后。
注意:
1 . wait方法和notify方法 notifyall方法,必须放在同步代码块或者同步方法里才生效。(因为在同步的基础上进行线程的通信才是有效的)
2 . sleep和wait的区别,sleep进入阻塞状态,没有释放锁;wait进入阻塞状态,释放了锁。
使用同步代码块,完成生产消费者通信代码存在的问题:
生产者和消费者在同一个等待池里,如果同时存在多个消费者,则无法实现。解决思路,所有生产者放入一个等待池,所有消费者放入一个等待池。
使用lock 锁来实现线程通信:
lock 拥有一个同步池和多个等待池,synchronized拥有一个同步池和一个等待池。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!