多线程的神秘面纱...还在恐惧吗???

前言

最开始学习java时,头都大了,也没学好多线程,甚至都不明白啥是多线程...慢慢的不断学习,发现多线程其实并没有我们想象中的困难。

进程(Processes)与线程(Threads)

在操作系统里面,存在着非常多的进程与线程。在每个单核处理器中,某个时刻仅有一个线程在执行。但是为什么我们在平时使用中,却感觉是有多个线程在运行呢?因为处理器使用了时间分片技术。也就是将处理器的执行时间分割成很多片段,然后将每个时间片段分配给不一样的线程,由于处理器的运行速度非常快,所以就给我们造成一种假象,处理器在同时执行多个线程。随着科技的发展,现在出现了多核处理器电脑,并且每个核心可超线程执行任务,由于每个核都是独立的,大大提高了电脑的并发执行能力。

进程(Processes)

进程通常拥有自己的执行环境,特别的,还拥有私有的内存空间。在某种程度上看,进程可以看做是一个程序或者是应用,比如我们使用win7/win8的任务管理器,可查看到系统的进程列表。为了让多个进程之间能够正常通讯,现代的操作系统一般使用Inter Process Communication(IPC)技术,也称之为进程间通讯。通畅使用管道(Pipes)、Socket等。在java里面,一般一个jvm代表了一个进程。

线程(Threads)

线程有时候也叫做轻量级的进程。在一个进程中,包含了一个或者多个线程,这些线程共享进程的执行环境,资源,内存。所以创建线程以及销毁线程的代价却比创建进程小的多。

创建线程

方式1:实现Runnable接口

public class HelloRunnable implements Runnable {
    public void run() {
        System.out.println("Hello from a thread!");
    }
    public static void main(String args[]) {
        (new Thread(new HelloRunnable())).start();
    }
}

方式二:继承Thread类

public class HelloThread extends Thread {
    public void run() {
        System.out.println("Hello from a thread!");
    }
    public static void main(String args[]) {
        (new HelloThread()).start();
    }
}

注意点:启动线程使用的是start方法,不是调用run()方法。

在这两种方式中,我们应该使用哪种呢?

推荐方式一,因为这样子代码看上去更加简洁,并且灵活性更高。比如一个类本身已经继承了别的Thread,那么就无法实现多次继承Thread。

暂停线程的执行(Pausing Execution)

为什么需要暂停线程的执行呢?大家可以回想下,在使用ServerSocket时,有个accept方法,这个方法是阻塞的,也就是会在等待客户端的socket连接。所以线程的暂停有着非常多的用处,比如等待别的任务执行,等待IO完成,等待网络等。那么如何停止一个线程呢?直接使用Thread.sleep(int time)

public class SleepMessages {
    public static void main(String args[])
        throws InterruptedException {
        String importantInfo[] = {
            "Mares eat oats",
            "Does eat oats",
            "Little lambs eat ivy",
            "A kid will eat ivy too"
        };

        for (int i = 0;
             i < importantInfo.length;
             i++) {
            //Pause for 4 seconds
            Thread.sleep(4000);
            //Print a message
            System.out.println(importantInfo[i]);
        }
    }
}

注意点,Thread.sleep()方法会抛出一个InterruptedException,这个异常的抛出,是由于中断了这个线程,那么这个线程就不能继续存活下去了。

线程的中断

在一个线程中,可以执行各种操作,假设某个操作非常耗时,或者进入了长时间的睡眠,这时候想要中断这个线程,那么可以使用Thread.interrupted()来中断线程。被中断的线程会直接被杀死。

for (int i = 0; i < inputs.length; i++) {
    heavyCrunch(inputs[i]);
    if (Thread.interrupted()) {
        throw new InterruptedException();
    }
}

线程的Join

在多个线程中,如果一个线程调用了join(),那么主线程就会被停止,直到调用了join()的那个线程执行完毕。

public class ThreadDemo{
	public static void main(String[] args){
	    Thread mainThread = Thread.currentThread();
		System.out.println(mainThread.getName() + " start");
		Thread joinThread = new Thread(new Runnable(){
		    @Override
			public void run(){
			    System.out.println("I am joinThread");
				try{
		            Thread.sleep(5000);
		        }catch(InterruptedException e){
		            e.printStackTrace();
		        }
			}
		});
		joinThread.start();
		try{
		    joinThread.join();
		}catch(InterruptedException e){
		    e.printStackTrace();
		}
		//this partition code has to wait the joinThread to complete
	    System.out.println("end");
	}
}

在这个例子中,最后主线程部分的System.out.println("end");部分的代码,需要一直等待joinThread的执行完成,这里为了简单,只有一句输出,其实可以为任何的代码。

下面这个简单的例子,有两个线程,一个是MessageLoop线程,不断的输出数据,另外一个是Main Thread。Main Thread由jvm直接创建,所以每个程序至少有一个线程,那就是主线程。MessageLoop t启动之后,就不断的输出数据,然后主线程就不断的询问该线程t是否存活,如果存活就让t暂停1秒钟,这里使用了t.join(1000),然后判断程序的运行时间是否已经超过了patience标量,如果是,那就调用t.interrupt()方法来中断线程,最后主线程输出了Finally,表示结束。

public class SimpleThreads {

    // Display a message, preceded by
    // the name of the current thread
    static void threadMessage(String message) {
        String threadName =
            Thread.currentThread().getName();
        System.out.format("%s: %s%n",
                          threadName,
                          message);
    }

    private static class MessageLoop
        implements Runnable {
        public void run() {
            String importantInfo[] = {
                "Mares eat oats",
                "Does eat oats",
                "Little lambs eat ivy",
                "A kid will eat ivy too"
            };
            try {
                for (int i = 0;
                     i < importantInfo.length;
                     i++) {
                    // Pause for 4 seconds
                    Thread.sleep(4000);
                    // Print a message
                    threadMessage(importantInfo[i]);
                }
            } catch (InterruptedException e) {
                threadMessage("I wasn't done!");
            }
        }
    }

    public static void main(String args[])
        throws InterruptedException {

        // Delay, in milliseconds before
        // we interrupt MessageLoop
        // thread (default one hour).
        long patience = 1000 * 60 * 60;

        // If command line argument
        // present, gives patience
        // in seconds.
        if (args.length > 0) {
            try {
                patience = Long.parseLong(args[0]) * 1000;
            } catch (NumberFormatException e) {
                System.err.println("Argument must be an integer.");
                System.exit(1);
            }
        }

        threadMessage("Starting MessageLoop thread");
        long startTime = System.currentTimeMillis();
        Thread t = new Thread(new MessageLoop());
        t.start();

        threadMessage("Waiting for MessageLoop thread to finish");
        // loop until MessageLoop
        // thread exits
        while (t.isAlive()) {
            threadMessage("Still waiting...");
            // Wait maximum of 1 second
            // for MessageLoop thread
            // to finish.
            t.join(1000);
            if (((System.currentTimeMillis() - startTime) > patience)
                  && t.isAlive()) {
                threadMessage("Tired of waiting!");
                t.interrupt();
                // Shouldn't be long now
                // -- wait indefinitely
                t.join();
            }
        }
        threadMessage("Finally!");
    }
}

线程的同步

在线程中,由于资源、运行环境、内存等共享,所以会带来许多的问题,比如著名的死锁。下面的例子是一个简单的计数器

class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }

}

如果这个类的对象只是被一个线程访问,那么每次increment()、decrement(),c就会+1或者-1。但是如果这个对象同时被多个线程持有,并且并发的访问,那么程序的预期,将和我们所想象的不一致。上面的程序,使用的是c++,c--语法,但是并不代表c++,c--的执行流程只有一个指令。实际上,c++分为下面三个步骤。

1、获得当前c的值

2、当前的值+1

3、将第2步的值保存起来

下面我们假设Thread A,B同时访问increment(),那么很可能发生如下的过程:

1、Thread A获得c的值,c=0

2、Thread B获得c的值,c=0,

3、Thread A将值+1,c=1

4、Thread B将值-1,c=-1

5、Thread A将值保存起来,c=1

6、Thread B将值保存起来,c=-1

假设c执行之前为0,那么之后的结果会是多少呢?没错,是-1。这和我们预期的结果0并不一致。

那么是什么造成了这个问题呢?大家可以参考下其他文档,总体来说就是,多个线程对同一个变量的修改,对其他线程来说,可能是不可见的。比如Thread A对c的修改,Thread B是不可见的。

同步方法(Synchronized Methods)

在java中,提供了对同步方法的原语支持,关键字就是synchronized。如下例子:

public class SynchronizedCounter {
    private int c = 0;
    public synchronized void increment() {
        c++;
    }
    public synchronized void decrement() {
        c--;
    }
    public synchronized int value() {
        return c;
    }
}

在方法中加入了synchronized,那么系统就会保证如下的两个特性:

1、当一个线程访问一个synchronized方法时,其他方法只能等待。比如在上面例子中,Thread A假设在执行increment(),那么Thread B就必须等待Thread A的执行完毕。

2、一个线程执行完毕之后的结果,对于其他线程来说,结果是可见的。比如Thread A执行increment()之后,c的值为1,那么这时候Thread B获得的值也就是1。

内部锁(Intrinsic Locks)

锁,就是用来保护某一段代码只能被一个Thread访问。其他程序如果想要访问这段代码,就需要获得锁。使用synchronized methods,虚拟机会为整个方法都加入锁。在方法中加入synchronized,虽然实现了线程之间的同步,但是却降低了多线程的并发能力,如果一个方法中,仅仅只有一行代码需要同步,那么使用同步方法将会锁住整个方法,导致其他线程无法访问。所以在java中,支持同步代码块。语法也很简单,如下所示:

public void addName(String name) {

synchronized(this) {

lastName = name;

nameCount++;

}

nameList.add(name);

}

原子访问(Atomic Access)

在编程中,一个原子的动作,是不可分割的。类似于数据库中的原子操作,所有操作要么执行成功,要么执行失败,不存在半执行。在java中,大部分的原始数据类型的操作都是原子的,或者是加入了volatile关键字的域。一般而言,使用原子技术会让代码更加难以控制,理解起来也更加困难,所以应该避免大范围的使用。

死锁(Deadlock)

死锁就是两个线程互相等待对方的完成。下面的例子就说明了,两个friend都在等待对方bowBack(),所以程序会一直无限期的进入等待。

public class Deadlock {
    static class Friend {
        private final String name;
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public synchronized void bow(Friend bower) {
            System.out.format("%s: %s"
                + "  has bowed to me!%n",
                this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s"
                + " has bowed back to me!%n",
                this.name, bower.getName());
        }
    }

    public static void main(String[] args) {
        final Friend alphonse =
            new Friend("Alphonse");
        final Friend gaston =
            new Friend("Gaston");
        new Thread(new Runnable() {
            public void run() { alphonse.bow(gaston); }
        }).start();
        new Thread(new Runnable() {
            public void run() { gaston.bow(alphonse); }
        }).start();
    }
}

下面我们通过代码来演示一个生产者-消费者关系。这里一共有三个线程,一个是生产者,另外一个是消费者,另外一个是主线程。生产者负责生产,然后每次生产完之后,都需要notify消费者来取数据,消费者负责消费数据,每次消费完之后,都notify生产者开始生产数据。需要注意的一点是,生产者和消费者都是访问同一个对象,在例子中就是

Drop对象。
//可以假设为生产线
public class Drop {
    // Message sent from producer
    // to consumer.
    private String message;
    // True if consumer should wait
    // for producer to send message,
    // false if producer should wait for
    // consumer to retrieve message.
    private boolean empty = true;

    public synchronized String take() {
        // Wait until message is
        // available.
        while (empty) {
            try {
                wait();
            } catch (InterruptedException e) {}
        }
        // Toggle status.
        empty = true;
        // Notify producer that
        // status has changed.
        notifyAll();
        return message;
    }

    public synchronized void put(String message) {
        // Wait until message has
        // been retrieved.
        while (!empty) {
            try {
                wait();
            } catch (InterruptedException e) {}
        }
        // Toggle status.
        empty = false;
        // Store message.
        this.message = message;
        // Notify consumer that status
        // has changed.
        notifyAll();
    }
}

//生产者
import java.util.Random;

public class Producer implements Runnable {
    private Drop drop;

    public Producer(Drop drop) {
        this.drop = drop;
    }

    public void run() {
        String importantInfo[] = {
            "Mares eat oats",
            "Does eat oats",
            "Little lambs eat ivy",
            "A kid will eat ivy too"
        };
        Random random = new Random();

        for (int i = 0;
             i < importantInfo.length;
             i++) {
            drop.put(importantInfo[i]);
            try {
                Thread.sleep(random.nextInt(5000));
            } catch (InterruptedException e) {}
        }
        drop.put("DONE");
    }
}

//消费者
import java.util.Random;

public class Consumer implements Runnable {
    private Drop drop;

    public Consumer(Drop drop) {
        this.drop = drop;
    }

    public void run() {
        Random random = new Random();
        for (String message = drop.take();
             ! message.equals("DONE");
             message = drop.take()) {
            System.out.format("MESSAGE RECEIVED: %s%n", message);
            try {
                Thread.sleep(random.nextInt(5000));
            } catch (InterruptedException e) {}
        }
    }
}

//主线程
public class ProducerConsumerExample {
    public static void main(String[] args) {
        Drop drop = new Drop();
        (new Thread(new Producer(drop))).start();
        (new Thread(new Consumer(drop))).start();
    }
}

不可变对象

有时候,为了防止一些多线程问题的出现,可以使用不可变对象来代替可变对象。下面我们来看一个例子,一个颜色类。

public class SynchronizedRGB {

    // Values must be between 0 and 255.
    private int red;
    private int green;
    private int blue;
    private String name;

    private void check(int red,
                       int green,
                       int blue) {
        if (red < 0 || red > 255
            || green < 0 || green > 255
            || blue < 0 || blue > 255) {
            throw new IllegalArgumentException();
        }
    }

    public SynchronizedRGB(int red,
                           int green,
                           int blue,
                           String name) {
        check(red, green, blue);
        this.red = red;
        this.green = green;
        this.blue = blue;
        this.name = name;
    }

    public void set(int red,
                    int green,
                    int blue,
                    String name) {
        check(red, green, blue);
        synchronized (this) {
            this.red = red;
            this.green = green;
            this.blue = blue;
            this.name = name;
        }
    }

    public synchronized int getRGB() {
        return ((red << 16) | (green << 8) | blue);
    }

    public synchronized String getName() {
        return name;
    }

    public synchronized void invert() {
        red = 255 - red;
        green = 255 - green;
        blue = 255 - blue;
        name = "Inverse of " + name;
    }
}

这个类的getRGB()、getName()、invert()都是同步方法,但是还是可能会出现一些问题。比如下面的代码

SynchronizedRGB color =
    new SynchronizedRGB(0, 0, 0, "Pitch Black");
...
int myColorInt = color.getRGB();      //Statement 1
String myColorName = color.getName(); //Statement 2

在Stament1之后,如果一个线程在调用了getRGB()之后,另外一个方法调用set()方法,那么getName()和getRGB()得到的就不是同一个RGB颜色值了。那么我们如何防止这种情况的发生呢?没错,使用不可变对象的技术,去掉set()方法。

final public class ImmutableRGB {

    // Values must be between 0 and 255.
    final private int red;
    final private int green;
    final private int blue;
    final private String name;

    private void check(int red,
                       int green,
                       int blue) {
        if (red < 0 || red > 255
            || green < 0 || green > 255
            || blue < 0 || blue > 255) {
            throw new IllegalArgumentException();
        }
    }

    public ImmutableRGB(int red,
                        int green,
                        int blue,
                        String name) {
        check(red, green, blue);
        this.red = red;
        this.green = green;
        this.blue = blue;
        this.name = name;
    }

    public int getRGB() {
        return ((red << 16) | (green << 8) | blue);
    }

    public String getName() {
        return name;
    }

    public ImmutableRGB invert() {
        return new ImmutableRGB(255 - red,
                       255 - green,
                       255 - blue,
                       "Inverse of " + name);
    }
}

不可变对象的基本准则:

1、不提供setter方法修改fields

2、将所有fields变成private final

3、不允许子类override方法,可以使用final关键字修饰方法,或者使用private修饰

4、如果一个对象的field是一个引用对象,这个引用对象是可变的,那么就不允许修改该field,可以使用final修饰,并且不要将这个引用共享给其他对象,可以使用copy技术,将该引用对象的值赋值一份,再丢给其他对象。

使用java.util.concurrecy.*包来解决并发问题

在这个包中,有个很重要的抽象类,那就是Lock。这个方法的语义更加清晰,需要注意的一点就是,每次使用lock()之后,都需要在finally中,unlock()锁。下面使用Lock来改写前面死锁的例子:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.Random;

public class Safelock {
    static class Friend {
        private final String name;
        private final Lock lock = new ReentrantLock();

        public Friend(String name) {
            this.name = name;
        }

        public String getName() {
            return this.name;
        }

        public boolean impendingBow(Friend bower) {
            Boolean myLock = false;
            Boolean yourLock = false;
            try {
                myLock = lock.tryLock();
                yourLock = bower.lock.tryLock();
            } finally {
                if (! (myLock && yourLock)) {
                    if (myLock) {
                        lock.unlock();
                    }
                    if (yourLock) {
                        bower.lock.unlock();
                    }
                }
            }
            return myLock && yourLock;
        }

        public void bow(Friend bower) {
            if (impendingBow(bower)) {
                try {
                    System.out.format("%s: %s has"
                        + " bowed to me!%n",
                        this.name, bower.getName());
                    bower.bowBack(this);
                } finally {
                    lock.unlock();
                    bower.lock.unlock();
                }
            } else {
                System.out.format("%s: %s started"
                    + " to bow to me, but saw that"
                    + " I was already bowing to"
                    + " him.%n",
                    this.name, bower.getName());
            }
        }

        public void bowBack(Friend bower) {
            System.out.format("%s: %s has" +
                " bowed back to me!%n",
                this.name, bower.getName());
        }
    }

    static class BowLoop implements Runnable {
        private Friend bower;
        private Friend bowee;

        public BowLoop(Friend bower, Friend bowee) {
            this.bower = bower;
            this.bowee = bowee;
        }

        public void run() {
            Random random = new Random();
            for (;;) {
                try {
                    Thread.sleep(random.nextInt(10));
                } catch (InterruptedException e) {}
                bowee.bow(bower);
            }
        }
    }

    public static void main(String[] args) {
        final Friend alphonse =
            new Friend("Alphonse");
        final Friend gaston =
            new Friend("Gaston");
        new Thread(new BowLoop(alphonse, gaston)).start();
        new Thread(new BowLoop(gaston, alphonse)).start();
    }
}

在java.util.concurrent.*中,还提供了线程执行器,这个接口就是Executor。在之前的代码中,我们使用(new Thread(r)).start()来启动线程,有了执行器之后,可以使用e.execute(r)来启动一个线程。在Executor中,有两个比较重要的子类,ExecutorService和ScheduledExecutorService,具体的使用方式也比较简单,大家可以直接查看java doc便可。

线程池(Thread Pools)

如果你熟悉jdbc的操作,那么肯定知道数据库连接池。有关于池的概念,都很类似,就是把一些需要的对象,事先准备好,然后将这些对象放入内存中,以后每次需要的时候,就直接从内存中取出来,这样子可以大大节省new一个对象的开销。在java中,提供了几个不一样的线程池供我们选择。

1、java.util.concurrent.Executors.newCachedThreadPool()提供一个缓存池

2、java.util.concurrent.Executors.newSingleThreadExecutor()提供一个单线程池

3、java.util.concurrent.Executors.newFixedThreadPool()提供一个固定大小的线程池

支持多线程的容器(Concurrent Collections)

在java.util.Collection中,提供了很多容器类,但是这些类大部分都不是线程安全的,所以如果程序需要使用线程安全的容器类,那么可以使用java.util.concurrent包中的容器类,这些类最大的区别就是线程安全的,所以使用上对我们来说没什么新的难点,直接查看文档api便可。

原子变量(Atomic Variables)

在之前的例子中,说到了原子访问,比如下面的计数器:

class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }

}

一种方式可以让程序变得是线程安全的,一个是使用线程方法,如下代码:

class SynchronizedCounter {
    private int c = 0;
    public synchronized void increment() {
        c++;
    }
    public synchronized void decrement() {
        c--;
    }
    public synchronized int value() {
        return c;
    }
}

但是这种方式的效率不高,可以使用java.util.concurrent.atomic的原子类,如下代码:

import java.util.concurrent.atomic.AtomicInteger;

class AtomicCounter {
    private AtomicInteger c = new AtomicInteger(0);
    public void increment() {
        c.incrementAndGet();
    }
    public void decrement() {
        c.decrementAndGet();
    }
    public int value() {
        return c.get();
    }
}

总结:多线程的技术,一直是我们既向往又害怕的部分。向往是因为多线程的威力,害怕的是多线程的复杂性。在学习初,什么进程、线程、同步、原子、死锁等各种专业词汇搞得我们信心全无。只有不断的学习,总结,其实我们也可以掌握多线程。最后感谢大家的查询...继续苦逼写代码去了。

时间: 2024-10-01 10:53:36

多线程的神秘面纱...还在恐惧吗???的相关文章

ASP.NET 运行时详解 揭开请求过程神秘面纱

对于ASP.NET开发,排在前五的话题离不开请求生命周期.像什么Cache.身份认证.Role管理.Routing映射,微软到底在请求过程中干了哪些隐秘的事,现在是时候揭晓了.抛开乌云见晴天,接下来就一步步揭开请求管道神秘面纱. 上篇回顾 在介绍本篇内容之前,让我们先回顾下上一篇<ASP.NET运行时详解 集成模式和经典模式>的主要内容.在上一篇随笔中,我们提到ASP.NET运行时通过Application的InitInternal方法初始化运行管道.ASP.NET运行时提供了两种初始化管道模

揭开Sass和Compass的神秘面纱

可能之前你像我一样,对Sass和Compass毫无所知,好一点儿的可能知道它们是用来作为CSS预处理的.那么,今天请跟我一起学习下Sass和Compass的一些基础知识,包括它们是什么.如何安装.为什么要使用.基础语法等一些基本知识.需要说明的是我也仅仅只是刚刚接触Sass和Compass,一些高级用法等将不再本文的讨论范围之内.接触一周以后发现Sass和Compass的用处非常大,也打算今后在项目中尝试引进并应用起来.希望读完以后,你跟我一样对Sass和Compass给你带来的东西非常开心,也

【安全健行】(4):揭开shellcode的神秘面纱

2015/5/18 16:20:18 前面我们介绍了shellcode使用的基本策略,包括基本的shellcode.反向连接的shellcode以及查找套接字的shellcode.在宏观上了解了shellcode之后,今天我们来深入一步,看看shellcode到底是什么.也许大家和我一样,从接触安全领域就听说shellcode,也模糊地知道shellcode基本就是那个攻击载荷,但是shellcode到底长什么样,却一直遮遮掩掩,难睹真容.趁今天这个机会,我们一起来揭开shellcode的神秘面

iOS UIView动画实践(一):揭开Animation的神秘面纱

前言 在一个看脸的社会中,不论什么事物,长得好看总是能多吸引一些目光.App同样不例外,一款面相不错的App就算功能已经被轮子千百遍,依然会有人买账,理由就是看得顺眼,于是平面设计人员越来越被重视.白驹过隙,斗转星移,人们已然不满足于静态的美感,于是动态的用户体验应运而生,平面设计人员捉襟见肘,是我们程序员出马的时候了. 这篇文章是UIView Animation的第一篇,从极简的概念开始,为大家揭开Animation的神秘面纱.我们以一个登录界面为例.美丽的太阳,婀娜的云,还有几个小山包,中间

揭开RecyclerView的神秘面纱(一):RecyclerView的基本使用

前言 在Android开发中,我们经常与ListView.GridView打交道,它们为数据提供了列表和视图的展示方式,方便用户的操作.然而,随着Android的不断发展,单一的listview逐渐满足不了需求多变的项目了,因此,谷歌在support v7中,加入了新的控件--RecyclerView,该控件整合了ListView.GridView的特点,而且最大的优点是可以很方便实现瀑布流效果,因此RecyclerView受到越来越多的开发者重视.所以,学习RecyclerView的使用也是很

解开Redis的神秘面纱

本篇博文将为你解开Redis的神秘面纱,通过阅读本篇博文你将了解到以下内容: 什么是Redis? 为什么选择 Redis? 什么场景下用Redis? Redis 支持哪些语言? Redis下载 Redis 如何安装? windows 安装 Redis 如何修改配置? Redis Strings, Lists, Sets, Sorted Sets, Hashes 多种数据类型使用 1. 什么是Redis? 亚马逊技术社区解释 超快速的开源内存中数据存储,可用作数据库.缓存.消息代理和队列. Red

揭开webRTC媒体服务器的神秘面纱——WebRTC媒体服务器&amp;开源项目介绍

揭开webRTC媒体服务器的神秘面纱--WebRTC媒体服务器&开源项目介绍 WebRTC生态系统是非常庞大的.当我第一次尝试理解WebRTC时,网络资源之多让人难以置信.本文针对webRTC媒体服务器和相关的开源项目(如kurento,janus,jitsi.org等)做一些介绍.并且将尝试降低理解WebRTC的业务价值所需要的技术门槛. 何为WebRTC服务器? 自从WebRTC诞生之初以来,该技术的主要卖点之一是它可以进行点对点(browser-to-browser)通信,而几乎不需要服务

黑客是什么?揭开郭盛华的神秘面纱,讲解他不为人知传奇故事

今天小编给大家揭开白帽黑客.知名网络安全专家.东方联盟创始人郭盛华的神秘面纱和他不为人知的传奇故事.他不但电脑技术高超,还很爱国.直到今天,郭盛华品格的形成仍具有强大的影响力. 那么黑客到底是指什么?黑客技术.编写计算机代码的艺术和操纵计算机硬件一直是男人们在这个领域中的最高位置,这就是为什么许多年轻人向往的职业. 作为一个出身卑微的人,郭盛华没有任何贵族社会的条件.他唯一可以倚仗的只是自己出类拔萃的扭转不利局面的才华,这是一个网络专家和企业家必备的素质.正是关键时的一次心灵燃烧使他赢得了别人包

揭开SAP Fiori编程模型规范里注解的神秘面纱 - @OData.publish

今天是2020年2月1日鼠年大年初八,这是Jerry鼠年的第8篇文章,也是汪子熙公众号总共第207篇原创文章. Jerry的前一篇文章 揭开SAP Fiori编程模型规范里注解的神秘面纱 - @ObjectModel.readOnly工作原理解析,给大家分享了@ObjectModel.readOnly这个注解对应的Fiori UI和ABAP后台的工作原理. 今天我们继续研究另一个注解@OData.publish. 在SAP官网的ABAP Programming Model for SAP Fio