美文网首页
Java高并发系列——检视阅读(三)

Java高并发系列——检视阅读(三)

作者: 卡斯特梅的雨伞 | 来源:发表于2020-09-14 11:23 被阅读0次

    Java高并发系列——ReentrantLock

    ReentrantLock重入锁

    synchronized的局限性

    synchronized是java内置的关键字,它提供了一种独占的加锁方式。synchronized的获取和释放锁由jvm实现,用户不需要显示的释放锁,非常方便,然而synchronized也有一定的局限性,例如:

    1. 当线程尝试获取锁的时候,如果获取不到锁会一直阻塞,这个阻塞的过程,用户无法控制。(Synchronized不可中断的说法:只有获取到锁之后才能中断,等待锁时不可中断。
    2. 如果获取锁的线程进入休眠或者阻塞,除非当前线程异常,否则其他线程尝试获取锁必须一直等待。(synchronized不能响应中断?)

    synchronized不能响应中断参考

    ReentrantLock

    ReentrantLock是Lock的默认实现,在聊ReentranLock之前,我们需要先弄清楚一些概念:

    1. 可重入锁:可重入锁是指同一个线程可以多次获得同一把锁;ReentrantLock和关键字Synchronized都是可重入锁
    2. 可中断锁:可中断锁是指线程在获取锁的过程中,是否可以响应线程中断操作。synchronized是不可中断的,ReentrantLock是可中断的
    3. 公平锁和非公平锁:公平锁是指多个线程尝试获取同一把锁的时候,获取锁的顺序按照线程到达的先后顺序获取,而不是随机插队的方式获取。synchronized是非公平锁,而ReentrantLock是两种都可以实现,不过默认是非公平锁。

    ReentrantLock基本使用

    ReentrantLock的使用过程:

    1. 创建锁:ReentrantLock lock = new ReentrantLock();
    2. 获取锁:lock.lock()
    3. 释放锁:lock.unlock();

    对比上面的代码,与关键字synchronized相比,ReentrantLock锁有明显的操作过程,开发人员必须手动的指定何时加锁,何时释放锁,正是因为这样手动控制,ReentrantLock对逻辑控制的灵活度要远远胜于关键字synchronized,上面代码需要注意lock.unlock()一定要放在finally中,否则若程序出现了异常,锁没有释放,那么其他线程就再也没有机会获取这个锁了。

    ReentrantLock是可重入锁

    假如ReentrantLock是不可重入的锁,那么同一个线程第2次获取锁的时候由于前面的锁还未释放而导致死锁,程序是无法正常结束的。

    1. lock()方法和unlock()方法需要成对出现,锁了几次,也要释放几次,否则后面的线程无法获取锁了;可以将add中的unlock删除一个事实,上面代码运行将无法结束
    2. unlock()方法放在finally中执行,保证不管程序是否有异常,锁必定会释放

    示例:

    public class ReentrantLockTest {
        private static int num = 0;
        private static Lock lock = new ReentrantLock();
        public static void add() {
            lock.lock();
            lock.lock();
            try {
                num++;
            } finally {
                //lock()方法和unlock()方法需要成对出现,锁了几次,也要释放几次,否则后面的线程无法获取锁
                lock.unlock();
                lock.unlock();
            }
        }
        public static class T extends Thread {
            public T(String name) {
                super(name);
            }
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    ReentrantLockTest.add();
                }
            }
        }
        public static void main(String[] args) throws InterruptedException {
            T t1 = new T("t1");
            T t2 = new T("t2");
            T t3 = new T("t3");
            t1.start();
            t2.start();
            t3.start();
            t1.join();
            t2.join();
            t3.join();
            System.out.println("get num =" + num);
        }
    }
    //输出: get num =3000
    

    ReentrantLock实现公平锁

    在大多数情况下,锁的申请都是非公平的。这就好比买票不排队,上厕所不排队。最终导致的结果是,有些人可能一直买不到票。而公平锁,它会按照到达的先后顺序获得资源。公平锁的一大特点是不会产生饥饿现象,只要你排队,最终还是可以等到资源的;synchronized关键字默认是有jvm内部实现控制的,是非公平锁。而ReentrantLock运行开发者自己设置锁的公平性,可以实现公平和非公平锁。

    看一下jdk中ReentrantLock的源码,2个构造方法:

    public ReentrantLock() {    sync = new NonfairSync();}
    public ReentrantLock(boolean fair) {    sync = fair ? new FairSync() : new NonfairSync();}
    

    默认构造方法创建的是非公平锁

    第2个构造方法,有个fair参数,当fair为true的时候创建的是公平锁,公平锁看起来很不错,不过要实现公平锁,系统内部肯定需要维护一个有序队列,因此公平锁的实现成本比较高,性能相对于非公平锁来说相对低一些。因此,在默认情况下,锁是非公平的,如果没有特别要求,则不建议使用公平锁。

    示例:

    public class ReentrantLockFairTest {
        private static int num = 0;
        //private static Lock lock = new ReentrantLock(false);
        private static Lock lock = new ReentrantLock(true);
        public static class T extends Thread {
            public T(String name) {
                super(name);
            }
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    lock.lock();
                    try {
                        System.out.println(Thread.currentThread().getName()+" got lock");
                    } finally {
                        lock.unlock();
                    }
                }
            }
        }
        public static void main(String[] args) throws InterruptedException {
            T t1 = new T("t1");
            T t2 = new T("t2");
            T t3 = new T("t3");
            t1.start();
            t2.start();
            t3.start();
        }
    }
    

    输出:

    公平锁:
    t1 got lock                          
    t1 got lock
    t2 got lock
    t2 got lock
    t3 got lock
    t3 got lock
    非公平锁:
    t1 got lock
    t3 got lock
    t3 got lock
    t2 got lock
    t2 got lock
    t1 got lock
    

    ReentrantLock获取锁的过程是可中断的——使用lockInterruptibly()和tryLock(long time, TimeUnit unit)有参方法时。

    对于synchronized关键字,如果一个线程在等待获取锁,最终只有2种结果:

    1. 要么获取到锁然后继续后面的操作
    2. 要么一直等待,直到其他线程释放锁为止

    而ReentrantLock提供了另外一种可能,就是在等的获取锁的过程中(发起获取锁请求到还未获取到锁这段时间内)是可以被中断的,也就是说在等待锁的过程中,程序可以根据需要取消获取锁的请求。拿李云龙平安县围点打援来说,当平安县城被拿下后,鬼子救援的部队再尝试救援已经没有意义了,这时候要请求中断操作。

    关于获取锁的过程中被中断,注意几点:

    1. ReentrankLock中必须使用实例方法 lockInterruptibly()获取锁时,在线程调用interrupt()方法之后,才会引发 InterruptedException异常
    2. 线程调用interrupt()之后,线程的中断标志会被置为true
    3. 触发InterruptedException异常之后,线程的中断标志有会被清空,即置为false
    4. 所以当线程调用interrupt()引发InterruptedException异常,中断标志的变化是:false->true->false

    实例:

    public class InterruptTest2 {
        private static ReentrantLock lock1 = new ReentrantLock();
        private static ReentrantLock lock2 = new ReentrantLock();
    
        public static class T1 extends Thread {
            int lock;
    
            public T1(String name, Integer lock) {
                super(name);
                this.lock = lock;
            }
    
            @Override
            public void run() {
                try {
                    if (lock == 1) {
                        lock1.lockInterruptibly();
                        TimeUnit.SECONDS.sleep(1);
                        lock2.lockInterruptibly();
                    } else {
                        lock2.lockInterruptibly();
                        TimeUnit.SECONDS.sleep(1);
                        lock1.lockInterruptibly();
                    }
                } catch (InterruptedException e) {
                    //线程发送中断信号触发InterruptedException异常之后,中断标志将被清空。
                    System.out.println(this.getName() + "中断标志:" + this.isInterrupted());
                    e.printStackTrace();
                } finally {
                    //ReentrantLock自有的方法,多态实现的Lock不能用
                    if (lock1.isHeldByCurrentThread()) {
                        lock1.unlock();
                    }
                    if (lock2.isHeldByCurrentThread()) {
                        lock2.unlock();
                    }
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            T1 t1 = new T1("thread1", 1);
            T1 t2 = new T1("thread2", 2);
            t1.start();
            t2.start();
            TimeUnit.SECONDS.sleep(1000);
            //不加interrupt()通过jstack查看线程堆栈信息,发现2个线程死锁了
            //"thread2":
            //  waiting for ownable synchronizer 0x000000076b782028, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
            //  which is held by "thread1"
            //"thread1":
            //  waiting for ownable synchronizer 0x000000076b782058, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
            //  which is held by "thread2"
            t1.interrupt();
        }
    }
    

    ReentrantLock锁申请等待限时

    ReentrantLock刚好提供了这样功能,给我们提供了获取锁限时等待的方法 tryLock(),可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。

    tryLock无参方法——tryLock()是立即响应的,中间不会有阻塞。

    看一下源码中tryLock方法:

    public boolean tryLock()
    
    tryLock有参方法

    该方法在指定的时间内不管是否可以获取锁,都会返回结果,返回true,表示获取锁成功,返回false表示获取失败。 此方法在执行的过程中,如果调用了线程的中断interrupt()方法,会触发InterruptedException异常。

    可以明确设置获取锁的超时时间:

    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
    

    关于tryLock()方法和tryLock(long timeout, TimeUnit unit)方法,说明一下:

    1. 都会返回boolean值,结果表示获取锁是否成功。
    2. tryLock()方法,不管是否获取成功,都会立即返回;而有参的tryLock方法会尝试在指定的时间内去获取锁,中间会阻塞的现象,在指定的时间之后会不管是否能够获取锁都会返回结果。
    3. tryLock()方法不会响应线程的中断方法;而有参的tryLock方法会响应线程的中断方法,而出发 InterruptedException异常,这个从2个方法的声明上可以可以看出来。

    ReentrantLock其他常用的方法

    1. isHeldByCurrentThread:实例方法,判断当前线程是否持有ReentrantLock的锁,上面代码中有使用过。
    获取锁的4种方法对比
    获取锁的方法 是否立即响应(不会阻塞) 是否响应中断
    lock() × ×
    lockInterruptibly() ×
    tryLock() ×
    tryLock(long timeout, TimeUnit unit) ×

    实例:

    public class ReentrantLockTest1 {
        private static ReentrantLock lock1 = new ReentrantLock();
    
        public static class T extends Thread {
            public T(String name) {
                super(name);
            }
            @Override
            public void run() {
                try {
                    System.out.println(this.getName()+"尝试获取锁");
                    if (lock1.tryLock(2,TimeUnit.SECONDS)){
                        System.out.println(this.getName()+"获取锁成功");
                        TimeUnit.SECONDS.sleep(3);
                    }else {
                        System.out.println(this.getName()+"获取锁失败");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    if(lock1.isHeldByCurrentThread()){
                        lock1.unlock();
                    }
                }
            }
        }
        public static void main(String[] args) throws InterruptedException {
            T t1 = new T("t1");
            T t2 = new T("t2");
            t1.start();
            t2.start();
    
        }
    }
    

    输出:

    lock1.tryLock()
    t1尝试获取锁
    t1获取锁成功
    t2尝试获取锁
    t2获取锁失败
    lock1.tryLock(2,TimeUnit.SECONDS)
    t1尝试获取锁
    t2尝试获取锁
    t1获取锁成功
    t2获取锁失败
    

    总结

    1. ReentrantLock可以实现公平锁和非公平锁
    2. ReentrantLock默认实现的是非公平锁
    3. ReentrantLock的获取锁和释放锁必须成对出现,锁了几次,也要释放几次
    4. 释放锁的操作必须放在finally中执行
    5. lockInterruptibly()实例方法可以响应线程的中断方法,调用线程的interrupt()方法时,lockInterruptibly()方法会触发 InterruptedException异常
    6. 关于 InterruptedException异常说一下,看到方法声明上带有 throwsInterruptedException,表示该方法可以相应线程中断,调用线程的interrupt()方法时,这些方法会触发 InterruptedException异常,触发InterruptedException时,线程的中断中断状态会被清除。所以如果程序由于调用 interrupt()方法而触发 InterruptedException异常,线程的标志由默认的false变为ture,然后又变为false
    7. 实例方法tryLock()获会尝试获取锁,会立即返回,返回值表示是否获取成功
    8. 实例方法tryLock(long timeout, TimeUnit unit)会在指定的时间内尝试获取锁,指定的时间内是否能够获取锁,都会返回,返回值表示是否获取锁成功,该方法会响应线程的中断
    疑问

    Q:可中断锁:可中断锁时线程在获取锁的过程中,是否可以相应线程中断操作。为什么synchronized是不可中断的,ReentrantLock是可中断的?

    首先,只有获取到锁之后才能中断,等待锁时不可中断。

    查看Thread.interrupt()源码发现,这里面的操作只是做了修改一个中断状态值为true,并没有显式声明抛出InterruptedException异常。因此:

    • 若线程被中断前,如果该线程处于非阻塞状态(未调用过wait,sleep,join方法),那么该线程的中断状态将被设为true, 除此之外,不会发生任何事。
    • 若线程被中断前,该线程处于阻塞状态(调用了wait,sleep,join方法),那么该线程将会立即从阻塞状态中退出,并抛出一个InterruptedException异常,同时,该线程的中断状态被设为false, 除此之外,不会发生任何事。

    所以说,Synchronized锁此时为轻量级锁或重量级锁,此时等待线程是在自旋运行或者已经是重量级锁导致的阻塞状态了(非调用了wait,sleep,join等方法的阻塞),只把中断状态设为true,没有抛出异常真正中断。

    ReentrantLock.lockInterruptibly()首次尝试获取锁之前就会判断是否应该中断,如果没有获取到锁,在自旋等待的时候也会继续判断中断状态。(代码里会判断中断状态,所有会响应中断。)

    synchronized不能响应中断参考

    JUC中的Condition对象

    Condition使用简介——实现等待/通知机制

    注意:在使用使用Condition.await()方法时,需要先获取Condition对象关联的ReentrantLock的锁;就像使用Object.wait()时必须在synchronized同步代码块内。

    从整体上来看Object的wait和notify/notify是与对象监视器配合完成线程间的等待/通知机制,而Condition与Lock配合完成等待通知机制,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。两者除了在使用方式上不同外,在功能特性上还是有很多的不同:

    1. Condition能够支持不响应中断,而通过使用Object方式不支持
    2. Condition能够支持多个等待队列(new 多个Condition对象),而Object方式只能支持一个
    3. Condition能够支持超时时间的设置,而Object不支持

    Condition由ReentrantLock对象创建,并且可以同时创建多个,Condition接口在使用前必须先调用ReentrantLock的lock()方法获得锁,之后调用Condition接口的await()将释放锁,并且在该Condition上等待,直到有其他线程调用Condition的signal()方法唤醒线程,使用方式和wait()、notify()类似。

    需要注意的时,当一个线程被signal()方法唤醒线程时,它第一个动作是去获取同步锁,注意这一点,而这把锁目前在调用signal()方法唤醒他的线程上,必须等其释放锁后才能得到争抢锁的机会。

    实例:

    public class ConditionTest {
    
        private static ReentrantLock lock = new ReentrantLock();
        private static Condition condition = lock.newCondition();
    
        public static void main(String[] args) {
            T1 t1 = new T1("TT1");
            T2 t2 = new T2("TT2");
            t1.start();
            t2.start();
        }
    
        static class T1 extends Thread {
            public T1(String name) {
                super(name);
            }
    
            @Override
            public void run() {
                lock.lock();
                System.out.println(this.getName() + " start");
                try {
                    System.out.println(this.getName() + " wait");
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
                System.out.println(this.getName() + " end");
            }
        }
    
        static class T2 extends Thread {
            public T2(String name) {
                super(name);
            }
    
            @Override
            public void run() {
                lock.lock();
                System.out.println(this.getName() + " start");
                System.out.println(this.getName() + " signal");
                condition.signal();
                System.out.println(this.getName() + " end");
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(this.getName() + " end,2 second later");
            }
        }
    }
    

    输出:

    TT1 start
    TT1 wait
    TT2 start
    TT2 signal
    TT2 end
    TT2 end,2 second later
    

    Condition常用方法

    和Object中wait类似的方法

    1. void await() throws InterruptedException:当前线程进入等待状态,如果在等待状态中被中断会抛出被中断异常;
    2. long awaitNanos(long nanosTimeout):当前线程进入等待状态直到被通知,中断或者超时
    3. boolean await(long time, TimeUnit unit) throws InterruptedException:同第二种,支持自定义时间单位,false:表示方法超时之后自动返回的,true:表示等待还未超时时,await方法就返回了(超时之前,被其他线程唤醒了)
    4. boolean awaitUntil(Date deadline) throws InterruptedException:当前线程进入等待状态直到被通知,中断或者到了某个时间
    5. void awaitUninterruptibly();:当前线程进入等待状态,不会响应线程中断操作,只能通过唤醒的方式让线程继续

    和Object的notify/notifyAll类似的方法

    1. void signal():唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回。
    2. void signalAll():与1的区别在于能够唤醒所有等待在condition上的线程

    Condition.await()过程中被打断

    调用condition.await()之后,线程进入阻塞中,调用t1.interrupt(),给t1线程发送中断信号,await()方法内部会检测到线程中断信号,然后触发 InterruptedException异常,线程中断标志被清除。从输出结果中可以看出,线程t1中断标志的变换过程:false->true->false

    await(long time, TimeUnit unit)超时之后自动返回

    t1线程等待2秒之后,自动返回继续执行,最后await方法返回false,await返回false表示超时之后自动返回

    await(long time, TimeUnit unit)超时之前被唤醒

    t1线程中调用 condition.await(5,TimeUnit.SECONDS);方法会释放锁,等待5秒,主线程休眠1秒,然后获取锁,之后调用signal()方法唤醒t1,输出结果中发现await后过了1秒(1、3行输出结果的时间差),await方法就返回了,并且返回值是true。true表示await方法超时之前被其他线程唤醒了。

    long awaitNanos(long nanosTimeout)超时返回

    t1调用await方法等待5秒超时返回,返回结果为负数,表示超时之后返回的

    //awaitNanos参数为纳秒,可以调用TimeUnit中的一些方法将时间转换为纳秒。
    long nanos = TimeUnit.SECONDS.toNanos(2);
    

    waitNanos(long nanosTimeout)超时之前被唤醒

    t1中调用await休眠5秒,主线程休眠1秒之后,调用signal()唤醒线程t1,await方法返回正数,表示返回时距离超时时间还有多久,将近4秒,返回正数表示,线程在超时之前被唤醒了。

    其他几个有参的await方法和无参的await方法一样,线程调用interrupt()方法时,这些方法都会触发InterruptedException异常,并且线程的中断标志会被清除。

    同一个锁支持创建多个Condition

    使用两个Condition来实现一个阻塞队列的例子:

    public class MyBlockingQueue<E> {
    
        //阻塞队列最大容量
        private int size;
        //队列底层实现
        private LinkedList<E> list = new LinkedList<>();
        private static Lock lock = new ReentrantLock();
        //队列满时的等待条件
        private static Condition fullFlag = lock.newCondition();
        //队列空时的等待条件
        private static Condition emptyFlag = lock.newCondition();
    
        public MyBlockingQueue(int size) {
            this.size = size;
        }
    
        public void enqueue(E e) throws InterruptedException {
            lock.lock();
            try {
                //队列已满,在fullFlag条件上等待
                while (list.size() == size) {
                    fullFlag.await();
                }
                //入队:加入链表末尾
                list.add(e);
                System.out.println("生产了" + e);
                //通知在emptyFlag条件上等待的线程
                emptyFlag.signal();
            } finally {
                lock.unlock();
            }
        }
    
    
        public E dequeue() throws InterruptedException {
            lock.lock();
            try {
                while (list.size() == 0) {
                    emptyFlag.await();
                }
                E e = list.removeFirst();
                System.out.println("消费了" + e);
                //通知在fullFlag条件上等待的线程
                fullFlag.signal();
                return e;
            } finally {
                lock.unlock();
            }
        }
    
        /**
         * 创建了一个阻塞队列,大小为3,队列满的时候,会被阻塞,等待其他线程去消费,队列中的元素被消费之后,会唤醒生产者,生产数据进入队列。上面代码将队列大小置为1,可以实现同步阻塞队列,生产1个元素之后,生产者会被阻塞,待消费者消费队列中的元素之后,生产者才能继续工作。
         * @param args
         */
        public static void main(String[] args) {
            MyBlockingQueue<Integer> queue = new MyBlockingQueue<>(1);
            for (int i = 0; i < 10; i++) {
                int finalI = i;
                Thread producer = new Thread(() -> {
                    try {
                        queue.enqueue(finalI);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
                producer.start();
            }
            for (int i = 0; i < 10; i++) {
                Thread consumer = new Thread(() -> {
                    try {
                        queue.dequeue();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
                consumer.start();
            }
        }
    }
    

    输出:

    生产了0
    消费了0
    生产了1
    消费了1
    。。。。
    生产了9
    消费了9
    

    Object的监视器方法与Condition接口的对比

    注意同步队列和等待队列的区别,同步队列表示在竞争一把锁的队列中,是处于阻塞或运行状态的队列。

    而等待队列是指被置为等待、超时等待状态的线程,这些是没有竞争锁的权限的,处于等待被唤醒的状态中。

    对比项 Object 监视器方法 Condition
    前置条件 获取对象的锁 调用Lock.lock获取锁,调用Lock.newCondition()获取Condition对象
    调用方式 直接调用,如:object.wait() 直接调用,如:condition.await()
    等待队列个数 一个 多个,使用多个condition实现
    当前线程释放锁并进入等待状态 支持 支持
    当前线程释放锁进入等待状态中不响应中断 不支持 支持
    当前线程释放锁并进入超时等待状态 支持 支持
    当前线程释放锁并进入等待状态到将来某个时间 不支持 支持
    唤醒等待队列中的一个线程 支持 支持
    唤醒等待队列中的全部线程 支持 支持

    总结

    1. 使用condition的步骤:创建condition对象,获取锁,然后调用condition的方法
    2. 一个ReentrantLock支持创建多个condition对象
    3. void await() throws InterruptedException;方法会释放锁,让当前线程等待,支持唤醒,支持线程中断
    4. void awaitUninterruptibly();方法会释放锁,让当前线程等待,支持唤醒,不支持线程中断
    5. long awaitNanos(longnanosTimeout)throws InterruptedException;参数为纳秒,此方法会释放锁,让当前线程等待,支持唤醒,支持中断。超时之后返回的,结果为负数;超时之前被唤醒返回的,结果为正数(表示返回时距离超时时间相差的纳秒数)
    6. boolean await (longtime,TimeUnitunit)throws InterruptedException;方法会释放锁,让当前线程等待,支持唤醒,支持中断。超时之后返回的,结果为false;超时之前被唤醒返回的,结果为true
    7. boolean awaitUntil(Datedeadline)throws InterruptedException;参数表示超时的截止时间点,方法会释放锁,让当前线程等待,支持唤醒,支持中断。超时之后返回的,结果为false;超时之前被唤醒返回的,结果为true
    8. void signal();会唤醒一个等待中的线程,然后被唤醒的线程会被加入同步队列,去尝试获取锁
    9. void signalAll();会唤醒所有等待中的线程,将所有等待中的线程加入同步队列,然后去尝试获取锁
    疑问:

    Q:Condition能够支持超时时间的设置,而Object不支持。Object不是有wait(long timeout)超时时间设置么?

    相关文章

      网友评论

          本文标题:Java高并发系列——检视阅读(三)

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