Java并发之线程管理(线程基础知识)

时间:2022-04-05 06:48:33
因为书中涵盖的知识点比较全,所以就以书中的目录来学习和记录。当然,学习书中知识的时候自己的思考和实践是最重要的。说到线程,脑子里大概知道是个什么东西,但很多东西都还是懵懵懂懂,这是最可怕的。所以想着细致的来学习一下,就从这本实战开始学习。
疑问点:什么时候会用到多线程?什么情况下使用多线程来解决问题比较合适?

线程的创建和运行

就像学习任何知识一样,要学线程,先得学一下线程是怎么声明(创建)和运行起来的。
一般来说,java创建线程有两种常用的方式(线程池后面再谈):

  1、继承Thread类,并且覆盖run()方法。

  2、实现Runnable接口类,使用带参数的Thread构造器来创建Thread对象,参数就是Runnable接口的一个对象。

那么创建完了怎么运行呢?调用run()方法?调用run()方法只是简单的 类对象调用自己的成员方法,那么怎么会开启线程呢?而且每一个线程还有自己的信息(线程名字,线程ID,优先级等)。那么应该是怎么运行呢?答案是调用start()方法。

来个实例更加直观:
  • 继承Thread类
 //创建
public class Thread2 extends Thread{
@Override
public void run() {
System.out.println("继承Thread类创建线程");
}
}
//运行
private static void createThread2() {
Thread2 t2 = new Thread2();
Thread thread = new Thread(t2);
thread.start();
}
  • 实现Runnable接口
 //创建
public class Thread1 implements Runnable {
@Override
public void run() {
System.out.println("实现Runnable接口创建线程");
}
}
//运行
private static void createThread1() {
Runnable t1 = new Thread1();
Thread thread = new Thread(t1);
thread.start();
}

线程信息的获取和设置

Thread类有一些保存信息的属性,这些属性可以用来标识线程,显示线程状态或者控制线程的优先级。其实这些信息在Thread类中都可以找到,也就是Thread类的一些成员变量。
  • ID:保存线程的唯一标识符
  • Name:线程名称
  • Priority:线程对象的优先级,线程优先级从1-10,1是最低优先级,10是最高优先级。

Java并发之线程管理(线程基础知识)

  • Status:线程状态。在Java中,线程的状态有6种:new(创建)、runnbale(运行)、blocked(阻塞)、waiting(等待)、time waiting和terminated(终止)。状态转换图:

Java并发之线程管理(线程基础知识)

 这个图和上面的六种状态有点差异,六种状态是在Thread类中的枚举类State中的六种枚举。
 public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
在记忆的时候可以像上图中所示的那样,线程的状态转换图总是会忘掉,可以类比来记忆。线程的物种状态 创建、就绪、运行、阻塞和终止状态 类比开车的过程。
创建--->把车从车库拿出来
就绪--->坐在驾驶位置上,发动,拉手刹
运行--->车启动,开始走
阻塞--->路口红灯需要停车等待
就绪--->红灯停止,拉手刹开车
终止--->到达目的地,停车
例子举的不是很恰当,其实状态转换主要还是就绪、运行和阻塞三个状态的切换。运行状态运行一段时间后因为某些原因转阻塞状态,阻塞状态解除后 不会立马到运行状态而是到就绪状态,只有就绪状态才有可能获取CPU调度然后重新 变为运行状态。

线程的中断

就是结束正在运行的线程。中断方式有以下几种:
  • 调用interrupt()方法,当前执行的线程就会被中断。
 task.interrupt();

判断一个线程是否被中断了,有两种方式:

  1. interrupted(),检查当前执行的线程是否被中断
  2. isInterrupted():当interrupt()方法被调用时,Thread类中表名线程是否被中断的属性会被设置为true。isInterrupted()方法返回这个属性的值。

推荐使用isInterrupted(),因为interrupted()方法是一个静态方法。

 public static boolean interrupted() {
return currentThread().isInterrupted(true);
}

线程中断的控制

如果线程实现了复杂的算法那并且分布在几个方法中,或者线程中有递归调用的方法,如何去中断线程?因为直接 调用interrupt()方法不能立竿见影。java还提供了InterruptException异常。当检查到线程中断的时候,就抛出异常,然后在run()方法中捕获并处理这个异常。
run()方法中捕获异常,打印线程信息
 @Override
public void run() {
File file = new File(initPath);
if (file.isDirectory()) {
try {
dirctoryProcess(file);
} catch (InterruptedException e) {
System.out.printf("%s:the search has been interrupted", Thread.currentThread().getName());
}
}
}
在dirctoryProcess()方法中抛出异常。
 private void dirctoryProcess(File file) throws InterruptedException {
File[] list = file.listFiles();
if (list != null) {
for (int i = 0; i < list.length; i++) {
if (list[i].isDirectory()) {
dirctoryProcess(list[i]);
} else {
fileProcess(list[i]);
}
}
}
if (Thread.interrupted()) {
throw new InterruptedException();
}
}

线程的休眠和恢复

java里面提供了sleep()方法来休眠线程。需要掌握的知识点:
  • 休眠指定时间后线程自动恢复
  • 线程休眠的方式有两种:
    • Thread.sleep(1000);//休眠一秒,其中这里的单位为ms
    • TimeUnit.SECONDS.sleep(1);//休眠一秒,这里的单位为s

等待线程的终止(join())

当一个线程对象的join()方法被调用时,调用它的线程将被挂起,直到这个线程对象完成它的任务。意思就是:如果在线程B中线程A调用了join()方法,那么只有A线程执行完毕后,才会接着执行线程B下面的代码。
DataSourceLoader睡眠3秒
 public class DataSourceLoader implements Runnable {
@Override
public void run() {
System.out.printf("Begining data sources loading: %s\n",new Date());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("Data Sources loading has finised:%s\n",new Date());
}
}
NetWorkConnectionLoader睡眠6秒
 public class NetWorkConnectionLoader implements Runnable {
@Override
public void run() {
System.out.printf("Begining data sources loading: %s\n",new Date());
try {
TimeUnit.SECONDS.sleep(6);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("Data Sources loading has finised:%s\n",new Date());
}
}
执行Main
 public class Main {
public static void main(String[] args) {
DataSourceLoader dsLoader = new DataSourceLoader();
Thread t1 = new Thread(dsLoader,"DataSourceThread");
NetWorkConnectionLoader ncLoader = new NetWorkConnectionLoader();
Thread t2 = new Thread(ncLoader,"NetWorkConnectionThread");
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("Main: Configuration has been loaded:%s\n",new Date());
}
}
输出结果如下:
 Begining data sources loading: Tue Apr 25 14:46:39 CST 2017
Begining data sources loading: Tue Apr 25 14:46:39 CST 2017
Data Sources loading has finised:Tue Apr 25 14:46:42 CST 2017
NetWorkConnectionLoader loading has finised:Tue Apr 25 14:46:45 CST 2017
Main: Configuration has been loaded:Tue Apr 25 14:46:45 CST 2017
从结果可以看出:对于main函数线程,如果t1,t2没有调用join函数(),则“
Main: Configuration has been loaded:Tue Apr 25 14:46:45 CST 2017
”这句话应该是先打印的,由于t1,t2调用join函数,结果是main要等待t1,t2执行完成后再执行main线程,即使t1,t2睡眠了一段时间。
java还提供了另外两种形式的join()方法:
  • join(long milliseconds)
  • join(long milliseconds,long nanos)

当一个线程调用其他线程的join()方法时,如果使用的是第一种join()方式,那么它不必等到被调用线程运行终止,如果参数指定的毫秒时钟已经到达,它将继续运行。例如:thread1中有这样的代码thread2.join(1000),thread1将挂起,知道满足下面两个条件之一:

  • thread2运行已经完成
  • 时钟已经过去了1000毫秒

当两个条件中的任何一个成立是,join()方法将返回。

第二种join()方法跟第一种相似,只是需要接受毫秒和纳秒两个参数。
其实join()方法在底层调用的就是join(long milliseconds)方法,只不过传递的值是0,即:
 public final void join() throws InterruptedException {
join(0);
}
join()底层实现有一个wait()方法,这样看就比较好记忆了
 public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}

守护线程的创建和运行

守护线程(Daemon):这种线程的优先级很低,通常来说,当一个应用程序中没有其他线程运行的时候,守护线程才运行(这个是守护线程的特性)。当守护线程是程序中唯一运行的线程时,守护线程执行结束后,JVM也就结束了。一个典型的守护线程是Java的垃圾回收器。
举的例子是一个队列中的数据插入和取出:
其中WriteTask负责向队列中插入数据,循环一百次,插入的内容为 时间和 字符串类型的 事件
 public class WriterTask implements Runnable {
Deque<Event> deque; public WriterTask (Deque<Event> deque){
this.deque=deque;
} @Override
public void run() { // Writes 100 events
for (int i=1; i<100; i++) {
// Creates and initializes the Event objects
Event event=new Event();
event.setDate(new Date());
event.setEvent(String.format("The thread %s has generated an event",Thread.currentThread().getId())); // Add to the data structure
deque.addFirst(event);
try {
// Sleeps during one second
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
 
记录队列中信息的类 Event
 public class Event {

     private Date date;
private String event; public Date getDate() {
return date;
} public void setDate(Date date) {
this.date = date;
} public String getEvent() {
return event;
} public void setEvent(String event) {
this.event = event;
}
}
取出数据的线程是一个守护线程 ClearTask
 public class CleanerTask extends Thread {
private Deque<Event> deque; public CleanerTask(Deque<Event> deque) {
this.deque = deque;
// Establish that this is a Daemon Thread
setDaemon(true);
} @Override
public void run() {
while (true) {
Date date = new Date();
clean(date);
}
} private void clean(Date date) {
long difference;
boolean delete; if (deque.size()==0) {
return;
} delete=false;
do {
Event e = deque.getLast();
difference = date.getTime() - e.getDate().getTime();
if (difference > 10000) {
System.out.printf("Cleaner: %s\n",e.getEvent());
deque.removeLast();
delete=true;
}
} while (difference > 10000);
if (delete){
System.out.printf("Cleaner: Size of the queue: %d\n",deque.size());
}
}
}
最后是执行的Main:
 public static void main(String[] args) {

         Deque<Event> deque=new ArrayDeque<Event>();

         WriterTask writer=new WriterTask(deque);
for (int i=0; i<3; i++){
Thread thread=new Thread(writer);
thread.start();
} CleanerTask cleaner=new CleanerTask(deque);
cleaner.start();
}
三个写线程,一个取线程,值得注意的是,只有当三个写线程都休眠的时候,取线程才开始工作。

线程中不可控异常的处理

java中异常Exception下面分两类,
  • 受检异常(非运行时异常)(Checked Exception)
  • 非受检异常(运行时异常)(UnChecked Exception)

由于线程的run()方法不能接受抛出异常,对于受检异常来说,可以在编写程序时捕获,对于非受检异常来说,因为不知道会不会抛出异常,这样就比较麻烦。好在Java提供了一种在线程对象里捕获和处理运行时异常的一种机制。具体如下:

实现用来处理运行时异常的类,这个类实现UnCaughtExceptionHandler接口并且实现这个接口的uncaughtException()方法
 public class ExceptionHandler implements UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.printf("An exception has been captured\n");
System.out.printf("Thread: %s\n",t.getId());
System.out.printf("Exception: %s: %s\n",e.getClass().getName(),e.getMessage());
System.out.printf("Stack Trace: \n");
e.printStackTrace(System.out);
System.out.printf("Thread status: %s\n",t.getState());
}
}
在run()方法里面制造运行时异常:
 @Override
public void run() {
// The next instruction always throws and exception
int numero=Integer.parseInt("TTT");
}
main函数如下:
 public static void main(String[] args) {
Task task=new Task();
Thread thread=new Thread(task);
thread.setUncaughtExceptionHandler(new ExceptionHandler());
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
} System.out.printf("Thread has finished\n");
}
这样就捕获了运行时异常,运行结果如下:
 An exception has been captured
Thread: 10
Exception: java.lang.NumberFormatException: For input string: "TTT"
Stack Trace:
java.lang.NumberFormatException: For input string: "TTT"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Integer.parseInt(Integer.java:580)
at java.lang.Integer.parseInt(Integer.java:615)
at com.packtpub.java7.concurrency.chapter1.recipe8.task.Task.run(Task.java:16)
at java.lang.Thread.run(Thread.java:745)
Thread status: RUNNABLE
Thread has finished
如果不捕获异常,那么输出结果如下:
 Exception in thread "Thread-0" java.lang.NumberFormatException: For input string: "TTT"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Integer.parseInt(Integer.java:580)
at java.lang.Integer.parseInt(Integer.java:615)
at com.packtpub.java7.concurrency.chapter1.recipe8.task.Task.run(Task.java:16)
at java.lang.Thread.run(Thread.java:745)
Thread has finished

线程局部变量的使用(ThreadLocal)

多线程中,共享数据是不安全的。为了保证共享数据的安全性有两种思路:
  1. 设置临界区,保证临界区里的数据一次只能有一个线程访问
  2. 为每个线程维护一个该共享数据的局部变量,这样,每个线程各自使用自己的局部变量。ThreadLocal就是这种思路的实现。
使用ThreadLocal的大致思路是:把共享数据包装在ThreadLocal<T>中。包装完成后ThreadLocal提供了取值和设值的方法。提供的方法有:
  • get():返回此线程局部变量的当前线程副本中的值。
  • set():将次线程局部变量的当前线程副本中的值设置为指定值。
  • remove():移除此线程局部变量当前线程的值。
  • initialValue():返回次线程局部变量的当前线程的“初始值”。线程第一次使用get()方法访问变量时将调用此方法,但如果线程之前调用了set(T)方法,则不会对该线程再调用initialValue()方法。通常,此方法对每个线程最多调用一次,但如果在调用get()后又调用了remove(),则可能再次调用此方法。

使用方式如下:

 public class SafeTask implements Runnable {

     private static ThreadLocal<Date> startDate= new ThreadLocal<Date>() {
protected Date initialValue(){
return new Date();
}
}; @Override
public void run() {
// Writes the start date
System.out.printf("Starting Thread: %s : %s\n",Thread.currentThread().getId(),startDate.get());
try {
TimeUnit.SECONDS.sleep((int)Math.rint(Math.random()*10));
} catch (InterruptedException e) {
e.printStackTrace();
}
// Writes the start date
System.out.printf("Thread Finished: %s : %s\n",Thread.currentThread().getId(),startDate.get());
}
}

线程的分组

Java提供了ThreadGroup类表示一组线程。线程组可以包含线程对象,也可以包含其他的线程组对象,它是一个树形结构。
线程组允许把一个组的线程当做一个单元,对组内线程对象进行访问并操作他们。对一些执行相同任务的线程,只要一个单一的调用,所有这些线程的运行都会被中断。
例子是这样的:创建十个线程并让他们休眠一个随机时间(这段时间比如执行了一个查询),当其中一个线程查找成功的时候,我们将中断其他的9个线程。
疑问点:线程组和线程之间是如何绑定的?
 ThreadGroup threadGroup = new ThreadGroup("Searcher");
SearchTask searchTask=new SearchTask(result);
for (int i=0; i<5; i++) {
Thread thread=new Thread(threadGroup, searchTask);
thread.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
如上所示,这样就绑定了线程searchTask和线程组threadGroup。线程组提供了一些方法,
activeCount():线程组中活动线程的估计数
interrupt():中断此线程中的所有线程
...其他方法以后用到的时候查看API
等待线程组中有线程满足结束条件
 private static void waitFinish(ThreadGroup threadGroup) {
while (threadGroup.activeCount()>9) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
中断线程组中的所有线程
 threadGroup.interrupt();

线程组中不可控异常的处理

在线程中处理不可控异常,上面的做法是编写一个类继承UncaughtExceptionHandler接口,然后使用thread.setUncaughtExceptionHandler(UncaughtExceptionHandler eh)来实现。因为Thread没有继承UncaughtExceptionHandler接口,而是提供了一个方法:
 public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
checkAccess();
uncaughtExceptionHandler = eh;
}
对于线程组来说,线程组直接实现了UncaughtExceptionHandler接口,
 class ThreadGroup implements Thread.UncaughtExceptionHandler {
这样的话,我们就可以声明一个类MyThreadGroup来继承ThreadGroup类,并实现UncaughtExceptionHandler接口唯一的方法:uncaughtException()方法
 public class MyThreadGroup extends ThreadGroup {
public MyThreadGroup(String name) {
super(name);
}
@Override
public void uncaughtException(Thread t, Throwable e) {
}
}
这样的话以后在使用MyThreadGroup的时候,出现了运行时异常,就可以交给已经实现的uncaughtException()方法处理了。

使用工厂类创建线程

Java提供了ThreadFactory接口,这个接口实现了线程对象工厂。
 public interface ThreadFactory {
Thread newThread(Runnable r);
}
工厂类就是用来创建对象的,那么如何使用线程工厂类呢?
其实从上面的接口看到,ThreadFactory定义了唯一的方法:newThread(Runnable r);方法需要一个Runnable类型的参数,这个参数就是我们定义的线程类。回顾一下不用线程工厂类的时候创建线程对象时怎么用的?
 public class Thread1 implements Runnable{
@override
public void run(){
}
}
Runnable t1 = new Thread1();
Thread t = new Thread(t1);
t.start();
使用了工厂之后呢?这么写,继承ThreadFactory接口实现newThread()方法,这里创建线程
 public class MyThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
Thread t=new Thread(r,"thread_name");
return t;
}
}
然后创建线程的时候这么写:
 Task task = new Task();
Thread thread=factory.newThread(task);
这么写有什么好处呢?创建线程集中在了new Thread()方法中。
  1. 更容易修改类,或者改变创建对象的方式
  2. 更容易为有限资源创建对象的数目,可以限制一个类型的对象的个数
  3. 更容易为创建的对象生成统计数据

Java并发之线程管理(线程基础知识)的更多相关文章

  1. Java做acm所需要的基础知识之排序问题

    Java做acm所需要的基础知识. 以前做acm的题都是用C/C++来写代码的,在学习完Java之后突然感觉Java中的方法比C/C++丰富很多,所以就整理一下平时做题需要用到的Java基础知识. 1 ...

  2. Java基础之线程——管理线程同步方法(BankOperation2&rpar;

    控制台程序. 当两个或多个线程共享同一资源时,例如文件或内存块,就需要采取措施,确保其中的一个线程不会修改另一个线程正在使用的资源.当其中的一个线程更新文件中的某个记录,同时另一个线程正在检索这个记录 ...

  3. 别指望一文读懂Java并发之从一个线程开始

    Understanding concurrent programming is on the same order of difficulty as understanding object-orie ...

  4. java笔记--用ThreadLocal管理线程,Callable&lt&semi;V&gt&semi;接口实现有返回值的线程

    用ThreadLocal管理线程,Callable<V>接口实现有返回值的线程 ThreadLocal在我的笔记"关于线程同步"的第5种方式里面有介绍,这里就不多说了. ...

  5. java并发之如何解决线程安全问题

    并发(concurrency)一个并不陌生的词,简单来说,就是cpu在同一时刻执行多个任务. 而Java并发则由多线程实现的. 在jvm的世界里,线程就像不相干的平行空间,串行在虚拟机中.(当然这是比 ...

  6. java第九节 网络编程的基础知识

    /** * * 网络编程的基础知识 * 网络协议与TCP/IP * IP地址和Port(端口号) * 本地回路的IP地址:127.0.0.1 * 端口号的范围为0-65535之间,0-1023之间的端 ...

  7. 学 Java 网络爬虫,需要哪些基础知识?

    说起网络爬虫,大家想起的估计都是 Python ,诚然爬虫已经是 Python 的代名词之一,相比 Java 来说就要逊色不少.有不少人都不知道 Java 可以做网络爬虫,其实 Java 也能做网络爬 ...

  8. &lpar;转&rpar;JAVA AJAX教程第二章-JAVASCRIPT基础知识

    开篇:JAVASCRIPT是AJAX技术中不可或缺的一部分,所以想学好AJAX以及现在流行的AJAX框架,学好JAVASCRIPT是最重要的.这章我给大家整理了一些JAVASCRIPT的基础知识.常用 ...

  9. Java基础之线程——管理线程同步代码块(BankOperation4)

    控制台程序. 除了同步类对象的方法之外,还可以把程序中的语句或代码块制定为synchronized,这种方式更强大,因为可以指定哪个对象从语句或代码块的同步中获益,而不像同步方法那样仅仅是包含代码的对 ...

  10. iOS&sol;OS X线程安全的基础知识

    处理多并发和可重入性问题,是每个库发展过程中面临的比较困难的挑战之一.在Parse平台上,我们尽最大的努力保证你在使用我的SDKs时所做的操作都是线程安全的,保证不会出现性能问题. 在这篇文章中我们将 ...

随机推荐

  1. 33个超级有用必须要收藏的PHP代码样例

    作为一个正常的程序员,会好几种语言是十分正常的,相信大部分程序员也都会编写几句PHP程序,如果是WEB程序员,PHP一定是必备的,即使你没用开发过大型软件项目,也一定多少了解它的语法. 在PHP的流行 ...

  2. &lpar;十二&rpar; WebGIS中矢量图层的设计

    文章版权由作者李晓晖和博客园共有,若转载请于明显处标明出处:http://www.cnblogs.com/naaoveGIS/. 1.前言 在前几章中我们已经了解了什么是矢量查询.屏幕坐标与地理坐标之 ...

  3. 烂泥:智能DNS使用与配置

    本文由ilanniweb提供友情赞助,首发于烂泥行天下 想要获得更多的文章,可以关注我的微信ilanniweb 公司的业务现在已经扩展到海外,对外提供的统一接口都是通过域名来解析的,但是海外用户访问国 ...

  4. C 语言学习 第二次作业总结

    本次作业内容,主要有以下几点: 新建 coding 帐号,且使用 coding 上传本次作业的代码 Printf及条件判断语句的使用 作业总结 作业总结: 同学们开始渐入佳境,能够较为流畅的写出合理的 ...

  5. mac mysql cmd

    sudo /usr/local/mysql/support-files/mysql.server start sudo /usr/local/mysql/support-files/mysql.ser ...

  6. cuda编程基础

    转自: http://blog.csdn.net/augusdi/article/details/12529247 CUDA编程模型 CUDA编程模型将CPU作为主机,GPU作为协处理器(co-pro ...

  7. 安装微软ASP&period;NET MVC 4,运行以下的包管理器控制台命令

    (菜鸟,勿喷,有错求指正)Asp.net  新建的类库中安装MVC4  .下面是步骤,1+2:打开程序包管理控制台,3:运行Install-Package Microsoft.AspNet.Mvc - ...

  8. State of Hyperparameter Selection

    State of Hyperparameter Selection DANIEL SALTIEL VIEW NOTEBOOK Historically hyperparameter determina ...

  9. Linux &lbrack;Ubuntu 12&period;0&period;1&rsqb; 常用命令

    1.文件名颜色的含义1)默认色代表普通文件.例:install.log2)绿色代表可执行文件.例:rc.news3)红色代表tar包 文件. 例:vim-7.1.tar.bz24)蓝色代表目录文件. ...

  10. 读书笔记:《重来REWORK》

    读书笔记:<重来REWORK> <重来Rework--更为简单有效的商业思维>这本书是看了别人的书单而购买的,初 拿到这本书翻看时,感觉有两点与平常的书不同,一是每个小节非常短 ...