美文网首页Java和基础JavaSE
线程、JMM、线程池、并发编程3大特性

线程、JMM、线程池、并发编程3大特性

作者: AIGame孑小白 | 来源:发表于2021-05-13 14:45 被阅读0次

    01 进程与线程的区别

    进程:是正在执行的一段程序,一旦程序被载入内存准备执行,那就是一个进程。进程是表示资源分配的基本概念,又是调度运算的基本单位,是系统的并发执行单元。

    线程:单个进程中执行的每个任务都是一个线程,线程是进程中执行运算的最小单位。

    线程的意义:提高程序效率,充分发挥多和计算机的优势。

    02 多线程创建

    1,继承Thread类,实现run();

    public static class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println("我的一个线程");
        }
    }
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
    }
    

    2,实现Runnable接口,将对象做为Thread的构造参数传入

    public static class MyRunnable implements Runnable{
        @Override
        public void run() {
            System.out.println("我的一个线程");
        }
    }
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start();
    }
    

    还有比较灵活的写法:

    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("匿名内部类写法");
        }
    });
    //JDK1.8版本以后
    Thread t2 = new Thread(()->{
        System.out.println("兰姆达表达式");
    });
    t1.start();
    t2.start();
    

    3,实现Callable接口的call(),允许线程执行完以后获取返回结果

    public static class MyCall implements Callable<String> {
        @Override
        public String call() throws Exception {
            return "返回结果";
        }
    }
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<String> task = new FutureTask<String>(new MyCall());
        Thread thread = new Thread(task);
        thread.start();
        System.out.println(task.get());
    }
    

    03 用户线程与守护线程

    当JVM运行起来以后,会有两种线程:用户线程与守护线程。

    JVM关闭的条件:当用户线程运行完毕后,即使存在守护线程,它也会关闭。所以不能绝对的说:用户线程和守护线程都运行完毕了JVM才关闭。

    在Java里有个很出名的守护线程就是:GC垃圾回收线程。

    做一个小测试:

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程1:"+i);
            }
        });
        //守护线程
        thread1.setDaemon(true);
        thread1.start();
        //用户线程
        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程2:"+i);
            }
        });
        thread2.start();
    }
    

    输出结果:

    线程2:94
    线程2:95
    线程1:25
    线程2:96
    线程2:97
    线程2:98
    线程1:26
    线程2:99
    Process finished with exit code 0
    

    上面的thread1.setDaemon(true);设置成了守护线程,当用户线程结束后,守护线程也停止运行了。

    04 线程的优先级

    CPU内核同一时刻只能执行一条线程,内核靠线程调度器来分配时间片来执行线程。所以线程需要抢夺时间片来执行,那么优先级其实就是设置抢夺的概率。

    public static void main(String[] args){
        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 100; i++) {
                System.out.println("线程1:"+i);
            }
        });
        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 100; i++) {
                System.out.println("线程2:"+i);
            }
        });
        thread1.setPriority(10);
        thread2.setPriority(5);
        thread1.start();
        thread2.start();
    }
    

    输出结果:

    线程1:97
    线程1:98
    线程1:99
    线程2:0
    线程2:1
    线程2:2
    线程2:3
    线程2:4
    

    可以看到线程1执行完毕后线程2才开始执行,说明线程1争夺时间片的概率大。

    05 线程的生命周期

    线程的生命周期

    06 join方法

    在线程执行的时候让别的线程调用join先插队,等别的线程执行完后再执行本线程。

    public static void main(String[] args){
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 3; i++) {
                System.out.println("t1:"+i);
            }
        });
        Thread t2 = new Thread(()->{
            try {
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int i = 0; i < 3; i++) {
                System.out.println("t2:"+i);
            }
        });
        Thread t3 = new Thread(()->{
            try {
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int i = 0; i < 3; i++) {
                System.out.println("t3:"+i);
            }
        });
        t1.start();
        t2.start();
        t3.start();
    }
    

    输出结果:

    t1:0
    t1:1
    t1:2
    t2:0
    t2:1
    t2:2
    t3:0
    t3:1
    t3:2
    Process finished with exit code 0
    

    07 JMM内存模型

    JMM(Java Memory Model)


    JMM内存模型

    线程是不能够直接修改主存内的数据的,而是先从主存中读取到自己的工作内存中创建副本,修改完成后写入到主内存,这就是JMM模型。

    这也是为什么多线程并发访问修改数据的时候为什么出现安全问题。

    08 并发编程的三大特性

    原子性

    一个操作或多个操作,要么全部执行并且执行过程不被打断,要么全部不执行(提供互斥访问,在同一时刻只有一个线程进行访问)

    可以通过加锁的方式。

    先看不加锁:

    static int num = 100;
    public static void main(String[] args){
        Runnable runnable = ()->{
            while (true){
                if(num>0){
                    num--;
                }else{
                    break;
                }
                System.out.println(num);
            }
        };
        new Thread(runnable).start();
        new Thread(runnable).start();
        new Thread(runnable).start();
    }
    

    输出了诡异的结果:

    1
    0
    98
    94
    Process finished with exit code 0
    

    如果加锁:

    static int num = 100;
    public static void main(String[] args){
        Object o = new Object();
        Runnable runnable = ()->{
            while (true){
                synchronized (o){
                    if(num>0){
                        num--;
                    }else{
                        break;
                    }
                    System.out.println(num);
                }
            }
        };
        new Thread(runnable).start();
        new Thread(runnable).start();
        new Thread(runnable).start();
    }
    

    输出结果:

    5
    4
    3
    2
    1
    0
    Process finished with exit code 0
    

    加锁需要传入一个对象,本程序中该对象是唯一的,如果直接写new Object()和不加锁效果一样。

    可见性

    public static boolean flag = false;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            System.out.println("线程一号启动");
            while (!flag){};
            System.out.println("线程一号结束");
        }).start();
        Thread.sleep(1000);
        new Thread(()->{
            flag = true;
            System.out.println("把flag修改为了true");
        }).start();
    }
    

    显然对于一号线程而言,当二号线程修改了flag的值后,一号线程并没有及时获得flag,也就是说flag对于一号线程是不可见的。flag的值没有得到及时的更新。

    有序性

    JVM为了提高效率,会做出一些优化,对指令进行重排序(happens-before),在单线程的情况下没有问题,但是多线程编程需要考虑有序性问题。

    volatile

    1,保证可见性

    2,屏蔽指令重排序

    3,但是保证不了原子性

    synchronized

    JDK1.0开始提供的关键字,重量级的锁,能够保证某个代码块在被执行时,其他线程不访问执行。

    synchronized必须使用对象做为锁,因为对象分为三部分:头部分里面有个锁的字段,synchronized就是利用该字段达到上锁的目的。

    注意:假如有两个方法,要想实现调用fun1时fun2不能被访问,必须使用同一个对象加锁

    public static Object o ;
    public static  void fun1(){
        synchronized (o){}
    }
    public static void fun2(){
        synchronized (o){}
    }
    

    Monitor

    JVM 是通过进入、退出对象监视器(Monitor)来实现对方法,同步块的同步。

    使用synchronized加锁定的代码块,在被编译后会形成对象监视器的入口(monitorenter)和出口(monitorexit)


    对象监视器

    使用对象Object做为锁的时候,当有线程访问同步代码块的时候,监视器入口(monitorenter)会去检查Object是否上锁,没有上锁,则让该线程访问,同时给该对象上锁,此时若有其他线程访问该代码块的时候,监视器入口通过对比Object,发现上锁了,就会让其他线程处于阻塞状态,当第条线程执行完毕退出(monitorexir)以后,会给该对象解锁,那么被阻塞的线程就可以接着访问,并保证了原子性。

    public class Demo {
        public static synchronized void get(){
            //是把Demo.class(当前类的字节码文件)做为对象加锁
        }
        public synchronized void to(){
            //是把this(当前this锁)做为对象枷锁
        }
        public static void main(String[] args) {
            Demo demo = new Demo();
            demo.to();
            Demo.get();
        }
    }
    

    对象如同锁,持有锁的线程可以在同步中执行,没有持有锁的线程,即使获取CPU的执行权,也进不去。

    优点:解决了线程安全问题

    缺点:多个线程需要判断锁,较为消耗资源,抢锁的资源

    09 J.U.C之Lock

    Lock

    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        //Lock是代码实现的接口,而synchronized是JVM底层实现的
        new Thread(()->{
            lock.lock();
            try{
                //加锁代码块
            }finally {
                //必须手动释放
                lock.unlock();
            }
        }).start();
    }
    

    trylock

    上锁部分不被访问的时候,去做别的事情,而不是像synchronized那样必须阻塞。

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    public class Demo {
        Lock lock = new ReentrantLock();
        public void insert(Thread thread){
            if(lock.tryLock()){
                //抢到锁了,就调用这里的方法
                try{
                    System.out.println(thread.getName()+"抢到锁啦");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                    System.out.println(thread.getName()+"释放锁啦");
                }
            }else{
                //如果没有抢到锁,就调用这里的方法
                System.out.println(Thread.currentThread()+"没有抢到锁啦");
            }
        }
        public static void main(String[] args) {
            Demo d = new Demo();
            new Thread(()->{
                d.insert(Thread.currentThread());
            }).start();
            new Thread(()->{
                d.insert(Thread.currentThread());
            }).start();
        }
    }
    

    Lock与synchronized的区别

    1)Lock是一个接口,而synchronized是一个关键字,是Java内置的实现,它可以直接修饰方法和代码块,而lock只能修饰代码块。

    2)synchronized在发生异常的时候,会自动释放线程占有的锁,因此不会导致死锁现象,而Lock发生异常以后,如果没有unLock()释放锁,很可能造成死锁现象,因此在使用Lock的时候需要在finally中释放锁。

    3)Lock可以让等待的线程响应中断,去干别的事情,而synchronized会让线程一直阻塞下去。

    4)通过Lock的trylock()可以知道有没有成功获取锁,而synchronized不行。

    5)Lock可以提高多线程进行读操作的效率(提供读写锁)。

    从性能上来说,如果竞争资源很激烈,Lock的性能远远大于synchronized。

    10 线程通信

    wait与notify使用两个线程交替打印1-100,其中一条线程只打印奇数,另外一个线程只打印偶数。

    public class Wait {
        static class Num{
            public int num = 1;//共享资源
        }
        static class J implements Runnable{
            public Num numObj;
            public J(Num n){
                this.numObj = n;
            }
            @Override
            public void run() {
                synchronized (numObj){
                    while (numObj.num<=100){
                        if(numObj.num%2!=0){
                            System.out.println("奇数====>"+numObj.num);
                            numObj.num++;
    
                        }else {
                            try {
                                numObj.notify();
                                numObj.wait();//wait()要写在代码块里面,因为它的作用是释放锁,并且等待
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        }
        static class O implements Runnable{
            public Num numObj;
            public O(Num n){
                this.numObj = n;
            }
            @Override
            public void run() {
                synchronized (numObj){
                while (numObj.num<=100) {
                    if (numObj.num % 2 == 0) {
                        System.out.println("偶数====>" + numObj.num);
                        numObj.num++;
                    } else {
                        try {
                            numObj.notify();
                            numObj.wait();//wait()要写在代码块里面,因为它的作用是释放锁,并且等待
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
                }
            }
        }
        public static void main(String[] args) {
            Num numObj = new Num();
            new Thread(new J(numObj)).start();//奇数线程
            new Thread(new O(numObj)).start();//偶数线程
        }
    }
    

    wait()的作用是,释放当前锁,并使当前线程处于等待状态,所以需要写在同步代码块里,notify()是唤醒一个处于等待状态的线程,本题只有两条线程,如果有多条线程,那么可以使用notifyAll()。

    这三个方法最终调用的都是JVM级的native方法。

    11 什么是线程池

    线程池是Java的并发框架,几乎所有的并发执行程序或者异步操作都可以使用线程池。

    1)降低资源消耗。通过重复使用已经创建好的线程降低创建和销毁的消耗。

    2)提高响应速度。当任务到达时,不需要等到线程创建后就可以立即执行。

    3)提高现成的可管理性。使用线程池可以进行统一的分配、调优、监控。

    12 线程池工作原理

    Executor是最基本的接口,其子接口:ExecutorService是工作中常用的接口
    其中一个很重要的实现类是ThreadPoolExecutor,通过它new出来
    
    import java.util.concurrent.*;
    
    public class Pool {
         static class MyRunnable implements Runnable{
            String info = "";
            public MyRunnable(String txt){
                this.info = txt;
            }
            @Override
            public void run() {
                System.out.println(this.info);
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        public static void main(String[] args) {
    //        int corePoolSize      核心线程数
    //        int maximumPoolSize   最大线程数
    //        long keepAliveTime    保持存活得时间
    //        TimeUnit unit         时间单位
    //        BlockingQueue<Runnable> workQueue     任务队列
    //        RejectedExecutionHandler handler      饱和策略
            ExecutorService es = new ThreadPoolExecutor(
                    2,//corePoolSize
                    5,//maximumPoolSize
                    10,//keepAliveTime
                    TimeUnit.SECONDS,//unit
                    new ArrayBlockingQueue<Runnable>(5),
                    new ThreadPoolExecutor.AbortPolicy());
            for (int i = 1; i <= 20; i++) {
                try{
                    es.execute(new MyRunnable("执行第"+i+"条线程"));
                }catch (Throwable e){
                    System.out.println("丢弃线程"+i);
                }
            }
            es.shutdown();
        }
    }
    
    线程池

    四个流程:

    1)判断核心线程数

    2)判断任务队列

    3)判断最大线程数(备胎线程)

    4)执行饱和策略

    存活时间参数:当最大线程数(备胎线程)执行完毕,并且没有新任务的前提下,只能存活(keepAliveTime unit )的时间,超过时间就会被释放掉。

    13 三种常见队列

    SynchronousQueue

    一次性只能装一个任务,其他任务处于阻塞状态,同时一次性只能取出一个任务,如果没有任务可以取出,也处于阻塞状态

    import java.util.concurrent.SynchronousQueue;
    
    public class Queue {
        public static void main(String[] args) {
            SynchronousQueue<Integer> queue = new SynchronousQueue<>();
            new Thread(()->{
                for (int i = 0; i < 20; i++) {
                    try {
                        queue.put(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("装入数据====>"+i);
                }
            }).start();
            new Thread(()->{
                for (int i = 0; i < 20; i++) {
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    try {
                        System.out.println("取出数据=====>"+queue.take());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
    //执行结果
    取出数据=====>0
    装入数据====>0
        隔了两秒
    取出数据=====>1
    装入数据====>1
        隔了两秒
    取出数据=====>2
    装入数据====>2‘
        隔了两秒
    取出数据=====>3
    装入数据====>3
    

    LinkedBlockingQueue

    瞬间装完所有的任务,然后慢慢取出

    import java.util.concurrent.LinkedBlockingQueue;
    
    public class Queue {
        public static void main(String[] args) {
            LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
            new Thread(()->{
                for (int i = 0; i < 20; i++) {
                    try {
                        queue.put(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("装入数据====>"+i);
                }
            }).start();
            new Thread(()->{
                for (int i = 0; i < 20; i++) {
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    try {
                        System.out.println("取出数据=====>"+queue.take());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
    //执行结果
    装入数据====>18
    装入数据====>19
        瞬间装完20个数据
    取出数据=====>0
        隔了两秒
    取出数据=====>1
        隔了两秒
    取出数据=====>2
        隔了两秒
    取出数据=====>3
    

    ArrayBlockingQueue

    设置填装大小,比如4,那么一次性装入4个,其余的取出一个就装一个

    import java.util.concurrent.ArrayBlockingQueue;
    
    public class Queue {
        public static void main(String[] args) {
            ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(4);
            new Thread(()->{
                for (int i = 0; i < 20; i++) {
                    try {
                        queue.put(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("装入数据====>"+i);
                }
            }).start();
            new Thread(()->{
                for (int i = 0; i < 20; i++) {
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    try {
                        System.out.println("取出数据=====>"+queue.take());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
    //执行结果
    装入数据====>0
    装入数据====>1
    装入数据====>2
    装入数据====>3
        瞬间完成
    取出数据=====>0
    装入数据====>4
        隔了两秒
    取出数据=====>1
    装入数据====>5
        隔了两秒
    

    14 饱和策略

    CallerRunsPolicy
        不抛弃任务,调用线程池的线程,帮助执行任务
        比如上面的main方法调用的线程池,16号线程就会在main中执行
    演示代码:
    import java.util.concurrent.*;
    
    public class Pool {
         static class MyRunnable implements Runnable{
            String info = "";
            public MyRunnable(String txt){
                this.info = txt;
            }
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+":"+this.info);
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        public static void main(String[] args) {
            ExecutorService es = new ThreadPoolExecutor(
                    2,
                    5,
                    10,
                    TimeUnit.SECONDS,
                    new ArrayBlockingQueue<Runnable>(10),
                    new ThreadPoolExecutor.CallerRunsPolicy());
            for (int i = 1; i <= 20; i++) {
                try{
                    es.execute(new MyRunnable("执行第"+i+"条线程"));
                }catch (Throwable e){
                    System.out.println("丢弃线程"+i);
                }
            }
            es.shutdown();
        }
    }
    执行结果:
    pool-1-thread-2:执行第2条线程
    pool-1-thread-1:执行第1条线程
    main:执行第16条线程
    pool-1-thread-3:执行第13条线程
    pool-1-thread-4:执行第14条线程
    pool-1-thread-5:执行第15条线程
    
    AbortPolicy(默认)
        当最大线程数满了以后,抛出异常,抛弃任务
    
    DiscardPolicy
        连异常都不抛,直接把任务丢了
    
    DiscardOldestPolicy
        连异常都不抛,直接把任务丢了
    

    15 线程池工具类

    工具类:Executors 快速创建线程池

    缓存线程池

    newCachedThreadPool

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class Es {
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newCachedThreadPool();
            for (int i = 0; i < 10; i++) {
                System.out.println("线程"+i);
                executorService.execute(()->{
                    for (int j = 0; j < 5; j++) {
                        System.out.println(Thread.currentThread().getName()+":"+j);
                    }
                });
            }
            executorService.shutdown();
        }
    }
    

    源码的实现:

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    

    可以发现:没有核心线程数,扔进去多少线程,就会创建多少线程,好处是线程复用以及可以及时释放线程,弊端就是,当任务量极大的时候,他就创建一大堆线程。

    定长线程池

    newFixedThreadPool

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class Es {
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newFixedThreadPool(2);
            for (int i = 0; i < 10; i++) {
                System.out.println("线程"+i);
                executorService.execute(()->{
                    for (int j = 0; j < 5; j++) {
                        System.out.println(Thread.currentThread().getName()+":"+j);
                    }
                });
            }
            executorService.shutdown();
        }
    }
    

    源码的实现:

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    

    单例线程池

    newSingleThreadExecutor

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class Es {
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newSingleThreadExecutor();
            for (int i = 0; i < 10; i++) {
                System.out.println("线程"+i);
                executorService.execute(()->{
                    for (int j = 0; j < 5; j++) {
                        System.out.println(Thread.currentThread().getName()+":"+j);
                    }
                });
            }
            executorService.shutdown();
        }
    }
    

    可以发现单例线程池,会由一个线程完成所有的任务。

    源码的实现:

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
    

    调度线程池

    ScheduledExecutorService:newScheduledThreadPool

    可以实现:延迟执行的线程池

    public static void main(String[] args) {
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
        //延迟执行的线程池
        scheduledExecutorService.schedule(()->{
            System.out.println("过去5秒种啦");
        },5, TimeUnit.SECONDS);//第二个参数是延迟时间,然后是时间单位
        scheduledExecutorService.shutdown();
    }
    

    可以实现:周期执行的线程池

    public static void main(String[] args) {
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
        //周期执行的线程池:5s之后开始执行任务,每隔2s重复执行一次
        scheduledExecutorService.scheduleAtFixedRate(()->{
            System.out.println("执行啦");
        },5,2, TimeUnit.SECONDS);//参数:延迟时间 间隔时间 时间单位
        //scheduledExecutorService.shutdown();这时候不要shutdown
    }
    

    相关文章

      网友评论

        本文标题:线程、JMM、线程池、并发编程3大特性

        本文链接:https://www.haomeiwen.com/subject/fctxjltx.html