I was scratching my head about this for a while too, and since Dave's explanation is a little hard to understand I'll post my (apparently working) code:

private class TabListener<T extends Fragment> implements ActionBar.TabListener {
    private Fragment mFragment;
    private Activity mActivity;
    private final String mTag;
    private final Class<T> mClass;

    public TabListener(Activity activity, String tag, Class<T> clz) {
        mActivity = activity;
        mTag = tag;
        mClass = clz;
        mFragment=mActivity.getFragmentManager().findFragmentByTag(mTag);
    }

    public void onTabSelected(Tab tab, FragmentTransaction ft) {
        if (mFragment == null) {
            mFragment = Fragment.instantiate(mActivity, mClass.getName());
            ft.replace(android.R.id.content, mFragment, mTag);
        } else {
            if (mFragment.isDetached()) {
                ft.attach(mFragment);
            }
        }
    }

    public void onTabUnselected(Tab tab, FragmentTransaction ft) {
        if (mFragment != null) {
            ft.detach(mFragment);
        }
    }

    public void onTabReselected(Tab tab, FragmentTransaction ft) {
    }
}

As you can see it's pretty much like the Android sample, apart from not detaching in the constructor, and using replace instead of add.

After much headscratching and trial-and-error I found that finding the fragment in the constructor seems to make the double onCreateView problem magically go away (I assume it just ends up being null for onTabSelected when called through the ActionBar.setSelectedNavigationItem() path when saving/restoring state).